graphile-bucket-provisioner-plugin 1.18.0 → 1.19.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/README.md +10 -8
- package/esm/index.d.ts +1 -1
- package/esm/index.js +1 -1
- package/esm/plugin.d.ts +5 -4
- package/esm/plugin.js +33 -112
- package/esm/types.d.ts +4 -4
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +2 -2
- package/plugin.d.ts +5 -4
- package/plugin.js +33 -112
- package/types.d.ts +4 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ queueing storage reconciliation.
|
|
|
5
5
|
|
|
6
6
|
The constructive-db `storage:provision_bucket` job is the only component that
|
|
7
7
|
mints physical S3 bucket names, provisions S3, and records `physical_name`.
|
|
8
|
-
This plugin resolves the logical bucket under RLS and
|
|
8
|
+
This plugin resolves the logical bucket under RLS and requests that job; it
|
|
9
9
|
does not call S3 or derive a physical name.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
@@ -32,16 +32,18 @@ mutation {
|
|
|
32
32
|
bucketId
|
|
33
33
|
bucketKey
|
|
34
34
|
physicalName
|
|
35
|
-
|
|
35
|
+
provisionRequestedAt
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
`provisionBucket` resolves the bucket row under RLS and
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
`provisionBucket` resolves the bucket row under RLS and calls the storage
|
|
41
|
+
module's `<buckets>_provision(bucket_id)`, which stamps the row's
|
|
42
|
+
`provision_requested_at` as the caller and returns the row. The bucket's UPDATE
|
|
43
|
+
job trigger then enqueues the same `storage:provision_bucket` job used on
|
|
44
|
+
INSERT, keyed `storage:provision_bucket:<uuid>` on the `bucket:<uuid>` queue, so
|
|
45
|
+
repeated requests while a job is waiting collapse into it. Failures are
|
|
46
|
+
returned as GraphQL errors.
|
|
45
47
|
|
|
46
48
|
The payload fields are:
|
|
47
49
|
|
|
@@ -50,7 +52,7 @@ The payload fields are:
|
|
|
50
52
|
| `bucketId` | Logical bucket row ID |
|
|
51
53
|
| `bucketKey` | Logical bucket key |
|
|
52
54
|
| `physicalName` | Recorded physical name, or `null` while reconciliation is pending |
|
|
53
|
-
| `
|
|
55
|
+
| `provisionRequestedAt` | When reconciliation was last requested |
|
|
54
56
|
|
|
55
57
|
There are no plugin options: S3 connection details, naming, CORS, versioning,
|
|
56
58
|
and physical-name recording belong to the reconciler.
|
package/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides an explicit `provisionBucket` reconciliation
|
|
4
|
+
* Provides an explicit `provisionBucket` reconciliation request mutation for
|
|
5
5
|
* PostGraphile v5.
|
|
6
6
|
*/
|
|
7
7
|
export { BucketProvisionerPlugin, createBucketProvisionerPlugin } from './plugin';
|
package/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides an explicit `provisionBucket` reconciliation
|
|
4
|
+
* Provides an explicit `provisionBucket` reconciliation request mutation for
|
|
5
5
|
* PostGraphile v5.
|
|
6
6
|
*/
|
|
7
7
|
export { BucketProvisionerPlugin, createBucketProvisionerPlugin } from './plugin';
|
package/esm/plugin.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adds S3 bucket provisioning support to PostGraphile v5:
|
|
5
5
|
*
|
|
6
|
-
* 1. `provisionBucket` mutation — explicitly
|
|
7
|
-
* logical bucket row in the database.
|
|
8
|
-
*
|
|
6
|
+
* 1. `provisionBucket` mutation — explicitly request reconciliation for a
|
|
7
|
+
* logical bucket row in the database. Stamps the bucket's
|
|
8
|
+
* `provision_requested_at` via RLS; the bucket's own job trigger then
|
|
9
|
+
* queues the same storage reconciler job used on INSERT.
|
|
9
10
|
*
|
|
10
11
|
* Detection: Uses the `@storageBuckets` smart tag on the codec (table).
|
|
11
12
|
* The storage module generator in constructive-db sets this tag on the
|
|
@@ -19,7 +20,7 @@ import type { GraphileConfig } from 'graphile-config';
|
|
|
19
20
|
* This plugin provides one reconciliation pathway:
|
|
20
21
|
*
|
|
21
22
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
22
|
-
* bucket key to
|
|
23
|
+
* bucket key to request reconciliation (or re-reconciliation) for the
|
|
23
24
|
* bucket. Protected by RLS on the buckets table.
|
|
24
25
|
*
|
|
25
26
|
*/
|
package/esm/plugin.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adds S3 bucket provisioning support to PostGraphile v5:
|
|
5
5
|
*
|
|
6
|
-
* 1. `provisionBucket` mutation — explicitly
|
|
7
|
-
* logical bucket row in the database.
|
|
8
|
-
*
|
|
6
|
+
* 1. `provisionBucket` mutation — explicitly request reconciliation for a
|
|
7
|
+
* logical bucket row in the database. Stamps the bucket's
|
|
8
|
+
* `provision_requested_at` via RLS; the bucket's own job trigger then
|
|
9
|
+
* queues the same storage reconciler job used on INSERT.
|
|
9
10
|
*
|
|
10
11
|
* Detection: Uses the `@storageBuckets` smart tag on the codec (table).
|
|
11
12
|
* The storage module generator in constructive-db sets this tag on the
|
|
@@ -21,24 +22,7 @@ import { extendSchema, gql } from 'graphile-utils';
|
|
|
21
22
|
* resolution in the explicit provisionBucket mutation).
|
|
22
23
|
*/
|
|
23
24
|
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
|
|
25
|
+
SELECT * FROM function_resolution.storage_modules()
|
|
42
26
|
`;
|
|
43
27
|
/**
|
|
44
28
|
* Run a query against a grafast `withPgClient` client.
|
|
@@ -73,7 +57,7 @@ function scopeKeyColumn(storageModule) {
|
|
|
73
57
|
* silently picking.
|
|
74
58
|
*/
|
|
75
59
|
async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
|
|
76
|
-
const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY
|
|
60
|
+
const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY);
|
|
77
61
|
const modules = result.rows;
|
|
78
62
|
if (modules.length === 0) {
|
|
79
63
|
throw new Error(`STORAGE_MODULE_NOT_PROVISIONED: database ${databaseId} has no storage modules`);
|
|
@@ -124,86 +108,22 @@ async function resolveDatabaseId(pgClient) {
|
|
|
124
108
|
const result = await runQuery(pgClient, `SELECT jwt_private.current_database_id() AS id`);
|
|
125
109
|
return result.rows[0]?.id ?? null;
|
|
126
110
|
}
|
|
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
111
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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.
|
|
112
|
+
* Request reconciliation through the storage module's generated
|
|
113
|
+
* `<private>.<buckets>_provision(bucket_id)`. It runs as the request role and
|
|
114
|
+
* stamps the bucket's `provision_requested_at` under the buckets table's own
|
|
115
|
+
* RLS; the bucket's UPDATE job trigger then enqueues the reconciler (keyed and
|
|
116
|
+
* queued on the bucket, so repeated requests collapse into one pending job).
|
|
117
|
+
* It returns the updated bucket row.
|
|
147
118
|
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
priority => 0${identityArgs}
|
|
155
|
-
)).id AS id`;
|
|
156
|
-
}
|
|
157
|
-
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);
|
|
202
|
-
const jobId = result.rows[0]?.id;
|
|
203
|
-
if (!jobId) {
|
|
204
|
-
throw new Error(`STORAGE_BUCKET_JOB_ID_MISSING: bucket ${bucket.id}`);
|
|
119
|
+
async function requestReconciliation(pgClient, storageModule, bucket) {
|
|
120
|
+
const provisionFunction = QuoteUtils.quoteQualifiedIdentifier(storageModule.private_schema, `${storageModule.buckets_table}_provision`);
|
|
121
|
+
const result = await runQuery(pgClient, `SELECT id, key, physical_name, to_json(provision_requested_at) #>> '{}' AS provision_requested_at FROM ${provisionFunction}($1::uuid)`, [bucket.id]);
|
|
122
|
+
const row = result.rows[0];
|
|
123
|
+
if (!row?.id) {
|
|
124
|
+
throw new Error(`STORAGE_BUCKET_PROVISION_EMPTY: bucket ${bucket.id}`);
|
|
205
125
|
}
|
|
206
|
-
return
|
|
126
|
+
return row;
|
|
207
127
|
}
|
|
208
128
|
// --- Plugin factory ---
|
|
209
129
|
/**
|
|
@@ -212,7 +132,7 @@ async function enqueueReconciliationJob(pgClient, storageModule, bucket) {
|
|
|
212
132
|
* This plugin provides one reconciliation pathway:
|
|
213
133
|
*
|
|
214
134
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
215
|
-
* bucket key to
|
|
135
|
+
* bucket key to request reconciliation (or re-reconciliation) for the
|
|
216
136
|
* bucket. Protected by RLS on the buckets table.
|
|
217
137
|
*
|
|
218
138
|
*/
|
|
@@ -231,22 +151,23 @@ export function createBucketProvisionerPlugin() {
|
|
|
231
151
|
}
|
|
232
152
|
|
|
233
153
|
type ProvisionBucketPayload {
|
|
234
|
-
"""The logical bucket row
|
|
154
|
+
"""The logical bucket row whose reconciliation was requested."""
|
|
235
155
|
bucketId: UUID!
|
|
236
156
|
bucketKey: String!
|
|
237
157
|
"""The physical bucket name already recorded, or null when reconciliation has not completed."""
|
|
238
158
|
physicalName: String
|
|
239
|
-
"""
|
|
240
|
-
|
|
159
|
+
"""When reconciliation was last requested for this bucket."""
|
|
160
|
+
provisionRequestedAt: Datetime!
|
|
241
161
|
}
|
|
242
162
|
|
|
243
163
|
extend type Mutation {
|
|
244
164
|
"""
|
|
245
165
|
Reconcile an S3 bucket for a logical bucket in the database.
|
|
246
|
-
|
|
247
|
-
storage:provision_bucket job used
|
|
248
|
-
|
|
249
|
-
GraphQL
|
|
166
|
+
Records the request on the bucket row via RLS; the bucket's job
|
|
167
|
+
trigger then enqueues the same storage:provision_bucket job used on
|
|
168
|
+
INSERT, and repeated requests collapse into one pending job. This is
|
|
169
|
+
idempotent for an already-reconciled bucket; failures become GraphQL
|
|
170
|
+
errors.
|
|
250
171
|
"""
|
|
251
172
|
provisionBucket(
|
|
252
173
|
input: ProvisionBucketInput!
|
|
@@ -282,12 +203,12 @@ export function createBucketProvisionerPlugin() {
|
|
|
282
203
|
throw new Error('BUCKET_NOT_FOUND');
|
|
283
204
|
}
|
|
284
205
|
const { storageModule, bucket } = resolution;
|
|
285
|
-
const
|
|
206
|
+
const provisioned = await requestReconciliation(pgClient, storageModule, bucket);
|
|
286
207
|
return {
|
|
287
|
-
bucketId:
|
|
288
|
-
bucketKey:
|
|
289
|
-
physicalName:
|
|
290
|
-
|
|
208
|
+
bucketId: provisioned.id,
|
|
209
|
+
bucketKey: provisioned.key,
|
|
210
|
+
physicalName: provisioned.physical_name,
|
|
211
|
+
provisionRequestedAt: provisioned.provision_requested_at,
|
|
291
212
|
};
|
|
292
213
|
});
|
|
293
214
|
});
|
package/esm/types.d.ts
CHANGED
|
@@ -14,15 +14,15 @@ export interface ProvisionBucketInput {
|
|
|
14
14
|
ownerId?: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Result of the provisionBucket
|
|
17
|
+
* Result of the provisionBucket mutation: the bucket row it stamped.
|
|
18
18
|
*/
|
|
19
19
|
export interface ProvisionBucketPayload {
|
|
20
|
-
/** The logical bucket row
|
|
20
|
+
/** The logical bucket row whose reconciliation was requested */
|
|
21
21
|
bucketId: string;
|
|
22
22
|
/** The logical bucket key */
|
|
23
23
|
bucketKey: string;
|
|
24
24
|
/** The physical name already recorded, or null while reconciliation is pending */
|
|
25
25
|
physicalName: string | null;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/** When reconciliation was last requested */
|
|
27
|
+
provisionRequestedAt: string;
|
|
28
28
|
}
|
package/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
3
3
|
*
|
|
4
|
-
* Provides an explicit `provisionBucket` reconciliation
|
|
4
|
+
* Provides an explicit `provisionBucket` reconciliation request mutation for
|
|
5
5
|
* PostGraphile v5.
|
|
6
6
|
*/
|
|
7
7
|
export { BucketProvisionerPlugin, createBucketProvisionerPlugin } from './plugin';
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Bucket Provisioner Plugin for PostGraphile v5
|
|
4
4
|
*
|
|
5
|
-
* Provides an explicit `provisionBucket` reconciliation
|
|
5
|
+
* Provides an explicit `provisionBucket` reconciliation request mutation for
|
|
6
6
|
* PostGraphile v5.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-bucket-provisioner-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
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",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"@types/node": "^22.19.11",
|
|
59
59
|
"makage": "^0.8.0"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "b6c80da2ba0532a4aaf0d0154901efa7911974d8"
|
|
62
62
|
}
|
package/plugin.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Adds S3 bucket provisioning support to PostGraphile v5:
|
|
5
5
|
*
|
|
6
|
-
* 1. `provisionBucket` mutation — explicitly
|
|
7
|
-
* logical bucket row in the database.
|
|
8
|
-
*
|
|
6
|
+
* 1. `provisionBucket` mutation — explicitly request reconciliation for a
|
|
7
|
+
* logical bucket row in the database. Stamps the bucket's
|
|
8
|
+
* `provision_requested_at` via RLS; the bucket's own job trigger then
|
|
9
|
+
* queues the same storage reconciler job used on INSERT.
|
|
9
10
|
*
|
|
10
11
|
* Detection: Uses the `@storageBuckets` smart tag on the codec (table).
|
|
11
12
|
* The storage module generator in constructive-db sets this tag on the
|
|
@@ -19,7 +20,7 @@ import type { GraphileConfig } from 'graphile-config';
|
|
|
19
20
|
* This plugin provides one reconciliation pathway:
|
|
20
21
|
*
|
|
21
22
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
22
|
-
* bucket key to
|
|
23
|
+
* bucket key to request reconciliation (or re-reconciliation) for the
|
|
23
24
|
* bucket. Protected by RLS on the buckets table.
|
|
24
25
|
*
|
|
25
26
|
*/
|
package/plugin.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Adds S3 bucket provisioning support to PostGraphile v5:
|
|
6
6
|
*
|
|
7
|
-
* 1. `provisionBucket` mutation — explicitly
|
|
8
|
-
* logical bucket row in the database.
|
|
9
|
-
*
|
|
7
|
+
* 1. `provisionBucket` mutation — explicitly request reconciliation for a
|
|
8
|
+
* logical bucket row in the database. Stamps the bucket's
|
|
9
|
+
* `provision_requested_at` via RLS; the bucket's own job trigger then
|
|
10
|
+
* queues the same storage reconciler job used on INSERT.
|
|
10
11
|
*
|
|
11
12
|
* Detection: Uses the `@storageBuckets` smart tag on the codec (table).
|
|
12
13
|
* The storage module generator in constructive-db sets this tag on the
|
|
@@ -25,24 +26,7 @@ const graphile_utils_1 = require("graphile-utils");
|
|
|
25
26
|
* resolution in the explicit provisionBucket mutation).
|
|
26
27
|
*/
|
|
27
28
|
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
|
|
29
|
+
SELECT * FROM function_resolution.storage_modules()
|
|
46
30
|
`;
|
|
47
31
|
/**
|
|
48
32
|
* Run a query against a grafast `withPgClient` client.
|
|
@@ -77,7 +61,7 @@ function scopeKeyColumn(storageModule) {
|
|
|
77
61
|
* silently picking.
|
|
78
62
|
*/
|
|
79
63
|
async function resolveBucketByKey(pgClient, databaseId, bucketKey, ownerId) {
|
|
80
|
-
const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY
|
|
64
|
+
const result = await runQuery(pgClient, ALL_STORAGE_MODULES_QUERY);
|
|
81
65
|
const modules = result.rows;
|
|
82
66
|
if (modules.length === 0) {
|
|
83
67
|
throw new Error(`STORAGE_MODULE_NOT_PROVISIONED: database ${databaseId} has no storage modules`);
|
|
@@ -128,86 +112,22 @@ async function resolveDatabaseId(pgClient) {
|
|
|
128
112
|
const result = await runQuery(pgClient, `SELECT jwt_private.current_database_id() AS id`);
|
|
129
113
|
return result.rows[0]?.id ?? null;
|
|
130
114
|
}
|
|
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
115
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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.
|
|
116
|
+
* Request reconciliation through the storage module's generated
|
|
117
|
+
* `<private>.<buckets>_provision(bucket_id)`. It runs as the request role and
|
|
118
|
+
* stamps the bucket's `provision_requested_at` under the buckets table's own
|
|
119
|
+
* RLS; the bucket's UPDATE job trigger then enqueues the reconciler (keyed and
|
|
120
|
+
* queued on the bucket, so repeated requests collapse into one pending job).
|
|
121
|
+
* It returns the updated bucket row.
|
|
151
122
|
*/
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
priority => 0${identityArgs}
|
|
159
|
-
)).id AS id`;
|
|
160
|
-
}
|
|
161
|
-
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);
|
|
206
|
-
const jobId = result.rows[0]?.id;
|
|
207
|
-
if (!jobId) {
|
|
208
|
-
throw new Error(`STORAGE_BUCKET_JOB_ID_MISSING: bucket ${bucket.id}`);
|
|
123
|
+
async function requestReconciliation(pgClient, storageModule, bucket) {
|
|
124
|
+
const provisionFunction = quotes_1.QuoteUtils.quoteQualifiedIdentifier(storageModule.private_schema, `${storageModule.buckets_table}_provision`);
|
|
125
|
+
const result = await runQuery(pgClient, `SELECT id, key, physical_name, to_json(provision_requested_at) #>> '{}' AS provision_requested_at FROM ${provisionFunction}($1::uuid)`, [bucket.id]);
|
|
126
|
+
const row = result.rows[0];
|
|
127
|
+
if (!row?.id) {
|
|
128
|
+
throw new Error(`STORAGE_BUCKET_PROVISION_EMPTY: bucket ${bucket.id}`);
|
|
209
129
|
}
|
|
210
|
-
return
|
|
130
|
+
return row;
|
|
211
131
|
}
|
|
212
132
|
// --- Plugin factory ---
|
|
213
133
|
/**
|
|
@@ -216,7 +136,7 @@ async function enqueueReconciliationJob(pgClient, storageModule, bucket) {
|
|
|
216
136
|
* This plugin provides one reconciliation pathway:
|
|
217
137
|
*
|
|
218
138
|
* 1. **Explicit `provisionBucket` mutation** — Call this mutation with a
|
|
219
|
-
* bucket key to
|
|
139
|
+
* bucket key to request reconciliation (or re-reconciliation) for the
|
|
220
140
|
* bucket. Protected by RLS on the buckets table.
|
|
221
141
|
*
|
|
222
142
|
*/
|
|
@@ -235,22 +155,23 @@ function createBucketProvisionerPlugin() {
|
|
|
235
155
|
}
|
|
236
156
|
|
|
237
157
|
type ProvisionBucketPayload {
|
|
238
|
-
"""The logical bucket row
|
|
158
|
+
"""The logical bucket row whose reconciliation was requested."""
|
|
239
159
|
bucketId: UUID!
|
|
240
160
|
bucketKey: String!
|
|
241
161
|
"""The physical bucket name already recorded, or null when reconciliation has not completed."""
|
|
242
162
|
physicalName: String
|
|
243
|
-
"""
|
|
244
|
-
|
|
163
|
+
"""When reconciliation was last requested for this bucket."""
|
|
164
|
+
provisionRequestedAt: Datetime!
|
|
245
165
|
}
|
|
246
166
|
|
|
247
167
|
extend type Mutation {
|
|
248
168
|
"""
|
|
249
169
|
Reconcile an S3 bucket for a logical bucket in the database.
|
|
250
|
-
|
|
251
|
-
storage:provision_bucket job used
|
|
252
|
-
|
|
253
|
-
GraphQL
|
|
170
|
+
Records the request on the bucket row via RLS; the bucket's job
|
|
171
|
+
trigger then enqueues the same storage:provision_bucket job used on
|
|
172
|
+
INSERT, and repeated requests collapse into one pending job. This is
|
|
173
|
+
idempotent for an already-reconciled bucket; failures become GraphQL
|
|
174
|
+
errors.
|
|
254
175
|
"""
|
|
255
176
|
provisionBucket(
|
|
256
177
|
input: ProvisionBucketInput!
|
|
@@ -286,12 +207,12 @@ function createBucketProvisionerPlugin() {
|
|
|
286
207
|
throw new Error('BUCKET_NOT_FOUND');
|
|
287
208
|
}
|
|
288
209
|
const { storageModule, bucket } = resolution;
|
|
289
|
-
const
|
|
210
|
+
const provisioned = await requestReconciliation(pgClient, storageModule, bucket);
|
|
290
211
|
return {
|
|
291
|
-
bucketId:
|
|
292
|
-
bucketKey:
|
|
293
|
-
physicalName:
|
|
294
|
-
|
|
212
|
+
bucketId: provisioned.id,
|
|
213
|
+
bucketKey: provisioned.key,
|
|
214
|
+
physicalName: provisioned.physical_name,
|
|
215
|
+
provisionRequestedAt: provisioned.provision_requested_at,
|
|
295
216
|
};
|
|
296
217
|
});
|
|
297
218
|
});
|
package/types.d.ts
CHANGED
|
@@ -14,15 +14,15 @@ export interface ProvisionBucketInput {
|
|
|
14
14
|
ownerId?: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Result of the provisionBucket
|
|
17
|
+
* Result of the provisionBucket mutation: the bucket row it stamped.
|
|
18
18
|
*/
|
|
19
19
|
export interface ProvisionBucketPayload {
|
|
20
|
-
/** The logical bucket row
|
|
20
|
+
/** The logical bucket row whose reconciliation was requested */
|
|
21
21
|
bucketId: string;
|
|
22
22
|
/** The logical bucket key */
|
|
23
23
|
bucketKey: string;
|
|
24
24
|
/** The physical name already recorded, or null while reconciliation is pending */
|
|
25
25
|
physicalName: string | null;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/** When reconciliation was last requested */
|
|
27
|
+
provisionRequestedAt: string;
|
|
28
28
|
}
|