graphile-upload-plugin 1.1.0 โ 2.2.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 +1 -80
- package/esm/index.d.ts +31 -0
- package/esm/index.js +30 -4
- package/esm/plugin.d.ts +32 -0
- package/esm/plugin.js +247 -145
- package/esm/preset.d.ts +35 -0
- package/esm/preset.js +38 -0
- package/esm/types.d.ts +70 -0
- package/esm/types.js +1 -0
- package/index.d.ts +31 -4
- package/index.js +34 -9
- package/package.json +17 -24
- package/plugin.d.ts +32 -31
- package/plugin.js +249 -145
- package/preset.d.ts +35 -0
- package/preset.js +41 -0
- package/types.d.ts +70 -0
- package/types.js +2 -0
- package/esm/resolvers/upload.js +0 -55
- package/resolvers/upload.d.ts +0 -15
- package/resolvers/upload.js +0 -62
package/README.md
CHANGED
|
@@ -1,85 +1,6 @@
|
|
|
1
1
|
# graphile-upload-plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
<p align="center" width="100%">
|
|
8
|
-
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
|
|
9
|
-
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
|
|
10
|
-
</a>
|
|
11
|
-
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
|
|
12
|
-
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
|
|
13
|
-
</a>
|
|
14
|
-
<a href="https://www.npmjs.com/package/graphile-upload-plugin">
|
|
15
|
-
<img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-upload-plugin%2Fpackage.json"/>
|
|
16
|
-
</a>
|
|
17
|
-
</p>
|
|
18
|
-
|
|
19
|
-
**`graphile-upload-plugin`** adds an `Upload` scalar and upload field resolvers for PostGraphile, letting you store uploaded metadata in PostgreSQL columns.
|
|
20
|
-
|
|
21
|
-
## ๐ Installation
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
pnpm add graphile-upload-plugin
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## โจ Features
|
|
28
|
-
|
|
29
|
-
- Adds the `Upload` scalar to PostGraphile
|
|
30
|
-
- Supports upload resolvers by type or smart comment tag
|
|
31
|
-
- Flexible resolver hook to store files anywhere (S3, local, etc.)
|
|
32
|
-
|
|
33
|
-
## ๐ฆ Usage
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import express from 'express';
|
|
37
|
-
import { postgraphile } from 'postgraphile';
|
|
38
|
-
import UploadPostGraphilePlugin from 'graphile-upload-plugin';
|
|
39
|
-
|
|
40
|
-
const app = express();
|
|
41
|
-
app.use(
|
|
42
|
-
postgraphile(process.env.DATABASE_URL, ['app_public'], {
|
|
43
|
-
appendPlugins: [UploadPostGraphilePlugin],
|
|
44
|
-
graphileBuildOptions: {
|
|
45
|
-
uploadFieldDefinitions: [
|
|
46
|
-
{
|
|
47
|
-
name: 'upload',
|
|
48
|
-
namespaceName: 'public',
|
|
49
|
-
type: 'JSON',
|
|
50
|
-
resolve: async (upload, args, context, info) => {
|
|
51
|
-
// Handle upload
|
|
52
|
-
return { url: '...', size: upload.size };
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
tag: 'upload',
|
|
57
|
-
resolve: async (upload, args, context, info) => {
|
|
58
|
-
// Handle upload by tag
|
|
59
|
-
return { url: '...' };
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## ๐ง Configuration
|
|
69
|
-
|
|
70
|
-
The plugin accepts `uploadFieldDefinitions` in `graphileBuildOptions`:
|
|
71
|
-
|
|
72
|
-
- **By type**: Match PostgreSQL types by `name` and `namespaceName`
|
|
73
|
-
- **By tag**: Match columns via smart comments (e.g., `@upload`)
|
|
74
|
-
|
|
75
|
-
Each definition requires a `resolve` function that processes the upload and returns the value to store in the database.
|
|
76
|
-
|
|
77
|
-
## ๐งช Testing
|
|
78
|
-
|
|
79
|
-
```sh
|
|
80
|
-
# requires a local Postgres available (defaults to postgres/password@localhost:5432)
|
|
81
|
-
pnpm --filter graphile-upload-plugin test
|
|
82
|
-
```
|
|
3
|
+
File upload support for PostGraphile v5.
|
|
83
4
|
|
|
84
5
|
---
|
|
85
6
|
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Upload Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides file upload capabilities for PostGraphile v5 mutations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { UploadPlugin, UploadPreset } from 'graphile-upload-plugin';
|
|
9
|
+
*
|
|
10
|
+
* // Option 1: Use the preset (recommended)
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* UploadPreset({
|
|
14
|
+
* uploadFieldDefinitions: [
|
|
15
|
+
* { tag: 'upload', resolve: myUploadResolver },
|
|
16
|
+
* ],
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* };
|
|
20
|
+
*
|
|
21
|
+
* // Option 2: Use the plugin directly
|
|
22
|
+
* const plugin = UploadPlugin({
|
|
23
|
+
* uploadFieldDefinitions: [
|
|
24
|
+
* { tag: 'upload', resolve: myUploadResolver },
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export { UploadPlugin, createUploadPlugin } from './plugin';
|
|
30
|
+
export { UploadPreset } from './preset';
|
|
31
|
+
export type { FileUpload, UploadFieldDefinition, UploadPluginInfo, UploadPluginOptions, UploadResolver } from './types';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Upload Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides file upload capabilities for PostGraphile v5 mutations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { UploadPlugin, UploadPreset } from 'graphile-upload-plugin';
|
|
9
|
+
*
|
|
10
|
+
* // Option 1: Use the preset (recommended)
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* UploadPreset({
|
|
14
|
+
* uploadFieldDefinitions: [
|
|
15
|
+
* { tag: 'upload', resolve: myUploadResolver },
|
|
16
|
+
* ],
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* };
|
|
20
|
+
*
|
|
21
|
+
* // Option 2: Use the plugin directly
|
|
22
|
+
* const plugin = UploadPlugin({
|
|
23
|
+
* uploadFieldDefinitions: [
|
|
24
|
+
* { tag: 'upload', resolve: myUploadResolver },
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export { UploadPlugin, createUploadPlugin } from './plugin';
|
|
30
|
+
export { UploadPreset } from './preset';
|
package/esm/plugin.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Upload Plugin
|
|
3
|
+
*
|
|
4
|
+
* Adds file upload support to PostGraphile v5 mutations. For columns that match
|
|
5
|
+
* the configured upload field definitions (by PG type name/namespace or by smart
|
|
6
|
+
* tag), this plugin:
|
|
7
|
+
*
|
|
8
|
+
* 1. Registers a GraphQL `Upload` scalar type
|
|
9
|
+
* 2. Adds `*Upload` input fields on mutation input types
|
|
10
|
+
* 3. Wraps mutation field resolvers to process file uploads before the mutation
|
|
11
|
+
* executes, calling the user-supplied resolver for each upload
|
|
12
|
+
*
|
|
13
|
+
* In v5, the `GraphQLObjectType_fields_field` hook wraps the `resolve` function
|
|
14
|
+
* (which still exists on mutation fields alongside `plan`) to intercept uploads
|
|
15
|
+
* at the HTTP layer before the plan executes.
|
|
16
|
+
*
|
|
17
|
+
* COMPATIBILITY NOTE:
|
|
18
|
+
* This plugin uses v4-style resolver wrapping via GraphQLObjectType_fields_field hook.
|
|
19
|
+
* grafserv v5 supports this through its backwards-compatibility layer.
|
|
20
|
+
* This plugin requires grafserv's resolver support to be enabled (default in v5 RC).
|
|
21
|
+
* It will NOT work in a pure grafast plan-only execution context.
|
|
22
|
+
*/
|
|
23
|
+
import 'graphile-build';
|
|
24
|
+
import 'graphile-build-pg';
|
|
25
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
26
|
+
import type { UploadPluginOptions } from './types';
|
|
27
|
+
/**
|
|
28
|
+
* Creates the Upload plugin with the given options.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createUploadPlugin(options?: UploadPluginOptions): GraphileConfig.Plugin;
|
|
31
|
+
export declare const UploadPlugin: typeof createUploadPlugin;
|
|
32
|
+
export default UploadPlugin;
|
package/esm/plugin.js
CHANGED
|
@@ -1,158 +1,260 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Upload Plugin
|
|
3
|
+
*
|
|
4
|
+
* Adds file upload support to PostGraphile v5 mutations. For columns that match
|
|
5
|
+
* the configured upload field definitions (by PG type name/namespace or by smart
|
|
6
|
+
* tag), this plugin:
|
|
7
|
+
*
|
|
8
|
+
* 1. Registers a GraphQL `Upload` scalar type
|
|
9
|
+
* 2. Adds `*Upload` input fields on mutation input types
|
|
10
|
+
* 3. Wraps mutation field resolvers to process file uploads before the mutation
|
|
11
|
+
* executes, calling the user-supplied resolver for each upload
|
|
12
|
+
*
|
|
13
|
+
* In v5, the `GraphQLObjectType_fields_field` hook wraps the `resolve` function
|
|
14
|
+
* (which still exists on mutation fields alongside `plan`) to intercept uploads
|
|
15
|
+
* at the HTTP layer before the plan executes.
|
|
16
|
+
*
|
|
17
|
+
* COMPATIBILITY NOTE:
|
|
18
|
+
* This plugin uses v4-style resolver wrapping via GraphQLObjectType_fields_field hook.
|
|
19
|
+
* grafserv v5 supports this through its backwards-compatibility layer.
|
|
20
|
+
* This plugin requires grafserv's resolver support to be enabled (default in v5 RC).
|
|
21
|
+
* It will NOT work in a pure grafast plan-only execution context.
|
|
22
|
+
*/
|
|
23
|
+
import 'graphile-build';
|
|
24
|
+
import 'graphile-build-pg';
|
|
25
|
+
import { Transform } from 'stream';
|
|
26
|
+
/**
|
|
27
|
+
* Determines whether a codec attribute matches an upload field definition.
|
|
28
|
+
* Returns the matching definition or undefined.
|
|
29
|
+
*/
|
|
30
|
+
function relevantUploadType(attribute, uploadFieldDefinitions) {
|
|
31
|
+
const types = uploadFieldDefinitions.filter(({ name, namespaceName, tag }) => {
|
|
32
|
+
if (name && namespaceName) {
|
|
33
|
+
// Type-name based matching: check the attribute's codec PG extension metadata
|
|
34
|
+
const pgExt = attribute.codec?.extensions?.pg;
|
|
35
|
+
if (pgExt && pgExt.name === name && pgExt.schemaName === namespaceName) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Fallback: check codec name directly
|
|
39
|
+
if (attribute.codec?.name === name) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
13
42
|
}
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
if (tag) {
|
|
44
|
+
// Smart-tag based matching: check if the attribute has the specified tag
|
|
45
|
+
const tags = attribute.extensions?.tags;
|
|
46
|
+
if (tags && tags[tag]) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
16
49
|
}
|
|
17
|
-
return
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new GraphQLError('Upload serialization unsupported.');
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
addType(GraphQLUpload);
|
|
42
|
-
// Override the internal types for configured upload-backed columns
|
|
43
|
-
uploadFieldDefinitions.forEach(({ name, namespaceName, type }) => {
|
|
44
|
-
if (!name || !type || !namespaceName)
|
|
45
|
-
return; // tag-based or incomplete definitions
|
|
46
|
-
const theType = build.pgIntrospectionResultsByKind.type.find((typ) => typ.name === name && typ.namespaceName === namespaceName);
|
|
47
|
-
if (theType) {
|
|
48
|
-
build.pgRegisterGqlTypeByTypeId(theType.id, () => build.getTypeByName(type));
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
if (types.length === 1) {
|
|
53
|
+
return types[0];
|
|
54
|
+
}
|
|
55
|
+
else if (types.length > 1) {
|
|
56
|
+
throw new Error('Upload field definitions are ambiguous');
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function createSizeLimitStream(source, maxFileSize) {
|
|
61
|
+
let bytesRead = 0;
|
|
62
|
+
const limiter = new Transform({
|
|
63
|
+
transform(chunk, _encoding, callback) {
|
|
64
|
+
const chunkSize = Buffer.isBuffer(chunk)
|
|
65
|
+
? chunk.length
|
|
66
|
+
: Buffer.byteLength(String(chunk));
|
|
67
|
+
bytesRead += chunkSize;
|
|
68
|
+
if (bytesRead > maxFileSize) {
|
|
69
|
+
callback(new Error(`File exceeds maximum size of ${maxFileSize} bytes`));
|
|
70
|
+
return;
|
|
49
71
|
}
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
callback(null, chunk);
|
|
73
|
+
}
|
|
52
74
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
uploadColumn(attr) {
|
|
57
|
-
return this.column(attr) + 'Upload';
|
|
58
|
-
},
|
|
59
|
-
});
|
|
75
|
+
// Ensure source stream errors are always forwarded to the consumer stream.
|
|
76
|
+
source.on('error', (error) => {
|
|
77
|
+
limiter.destroy(error);
|
|
60
78
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!isPgRowType || !table || table.kind !== 'class') {
|
|
65
|
-
return fields;
|
|
79
|
+
limiter.on('error', (error) => {
|
|
80
|
+
if (!source.destroyed) {
|
|
81
|
+
source.destroy(error);
|
|
66
82
|
}
|
|
67
|
-
return build.extend(fields, table.attributes.reduce((memo, attr) => {
|
|
68
|
-
if (!build.pgColumnFilter(attr, build, context))
|
|
69
|
-
return memo;
|
|
70
|
-
const action = context.scope.isPgBaseInput
|
|
71
|
-
? 'base'
|
|
72
|
-
: context.scope.isPgPatch
|
|
73
|
-
? 'update'
|
|
74
|
-
: 'create';
|
|
75
|
-
if (build.pgOmit(attr, action))
|
|
76
|
-
return memo;
|
|
77
|
-
if (attr.identity === 'a')
|
|
78
|
-
return memo;
|
|
79
|
-
if (!relevantUploadType(attr)) {
|
|
80
|
-
return memo;
|
|
81
|
-
}
|
|
82
|
-
const fieldName = build.inflection.uploadColumn(attr);
|
|
83
|
-
if (memo[fieldName]) {
|
|
84
|
-
throw new Error(`Two columns produce the same GraphQL field name '${fieldName}' on class '${table.namespaceName}.${table.name}'; one of them is '${attr.name}'`);
|
|
85
|
-
}
|
|
86
|
-
memo = build.extend(memo, {
|
|
87
|
-
[fieldName]: context.fieldWithHooks(fieldName, {
|
|
88
|
-
description: attr.description,
|
|
89
|
-
type: build.getTypeByName('Upload'),
|
|
90
|
-
}, { pgFieldIntrospection: attr, isPgUploadField: true }),
|
|
91
|
-
}, `Adding field for ${build.describePgEntity(attr)}. You can rename this field with a 'Smart Comment':\n\n ${build.sqlCommentByAddingTags(attr, {
|
|
92
|
-
name: 'newNameHere',
|
|
93
|
-
})}`);
|
|
94
|
-
return memo;
|
|
95
|
-
}, {}), `Adding columns to '${build.describePgEntity(table)}'`);
|
|
96
83
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
source.pipe(limiter);
|
|
85
|
+
return limiter;
|
|
86
|
+
}
|
|
87
|
+
function isThenable(value) {
|
|
88
|
+
return (value !== null &&
|
|
89
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
90
|
+
typeof value.then === 'function');
|
|
91
|
+
}
|
|
92
|
+
function wrapUploadForMaxFileSize(upload, maxFileSize) {
|
|
93
|
+
if (!maxFileSize) {
|
|
94
|
+
return upload;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...upload,
|
|
98
|
+
createReadStream() {
|
|
99
|
+
const source = upload.createReadStream();
|
|
100
|
+
return createSizeLimitStream(source, maxFileSize);
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Add our new resolver which wraps the old resolver
|
|
130
|
-
async resolve(source, args, context, info) {
|
|
131
|
-
// Recursively check for Upload promises to resolve
|
|
132
|
-
async function resolvePromises(obj) {
|
|
133
|
-
for (const key of Object.keys(obj)) {
|
|
134
|
-
if (obj[key] instanceof Promise) {
|
|
135
|
-
if (uploadResolversByFieldName[key]) {
|
|
136
|
-
const upload = await obj[key];
|
|
137
|
-
// eslint-disable-next-line require-atomic-updates
|
|
138
|
-
obj[originals[key]] = await uploadResolversByFieldName[key](upload, args, context, {
|
|
139
|
-
...info,
|
|
140
|
-
uploadPlugin: { tags: tags[key], type: types[key] },
|
|
141
|
-
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Creates the Upload plugin with the given options.
|
|
106
|
+
*/
|
|
107
|
+
export function createUploadPlugin(options = {}) {
|
|
108
|
+
const { uploadFieldDefinitions = [], maxFileSize } = options;
|
|
109
|
+
return {
|
|
110
|
+
name: 'UploadPlugin',
|
|
111
|
+
version: '2.0.0',
|
|
112
|
+
description: 'File upload support for PostGraphile v5',
|
|
113
|
+
after: ['PgAttributesPlugin', 'PgMutationCreatePlugin', 'PgMutationUpdateDeletePlugin'],
|
|
114
|
+
schema: {
|
|
115
|
+
hooks: {
|
|
116
|
+
// Register the Upload scalar type
|
|
117
|
+
init(_, build) {
|
|
118
|
+
const { graphql: { GraphQLScalarType, GraphQLError } } = build;
|
|
119
|
+
const GraphQLUpload = new GraphQLScalarType({
|
|
120
|
+
name: 'Upload',
|
|
121
|
+
description: 'The `Upload` scalar type represents a file upload.',
|
|
122
|
+
parseValue(value) {
|
|
123
|
+
const maybe = value;
|
|
124
|
+
if (maybe &&
|
|
125
|
+
maybe.promise &&
|
|
126
|
+
typeof maybe.promise.then === 'function') {
|
|
127
|
+
return maybe.promise;
|
|
142
128
|
}
|
|
129
|
+
throw new GraphQLError('Upload value invalid.');
|
|
130
|
+
},
|
|
131
|
+
parseLiteral(_ast) {
|
|
132
|
+
throw new GraphQLError('Upload literal unsupported.');
|
|
133
|
+
},
|
|
134
|
+
serialize() {
|
|
135
|
+
throw new GraphQLError('Upload serialization unsupported.');
|
|
143
136
|
}
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
});
|
|
138
|
+
build.registerScalarType(GraphQLUpload.name, {}, () => GraphQLUpload, 'UploadPlugin registering Upload scalar');
|
|
139
|
+
return _;
|
|
140
|
+
},
|
|
141
|
+
// Add *Upload input fields alongside matching columns on mutation input types
|
|
142
|
+
GraphQLInputObjectType_fields(fields, build, context) {
|
|
143
|
+
const { scope: { pgCodec, isPgPatch, isPgBaseInput, isMutationInput } } = context;
|
|
144
|
+
// Only process row-based input types (create inputs, patch inputs, base inputs)
|
|
145
|
+
if (!pgCodec || !pgCodec.attributes) {
|
|
146
|
+
return fields;
|
|
147
|
+
}
|
|
148
|
+
// Must be a mutation-related input type
|
|
149
|
+
if (!isPgPatch && !isPgBaseInput && !isMutationInput) {
|
|
150
|
+
return fields;
|
|
151
|
+
}
|
|
152
|
+
const UploadType = build.getTypeByName('Upload');
|
|
153
|
+
if (!UploadType) {
|
|
154
|
+
return fields;
|
|
155
|
+
}
|
|
156
|
+
let newFields = fields;
|
|
157
|
+
for (const [attributeName, attribute] of Object.entries(pgCodec.attributes)) {
|
|
158
|
+
const matchedDef = relevantUploadType(attribute, uploadFieldDefinitions);
|
|
159
|
+
if (!matchedDef)
|
|
160
|
+
continue;
|
|
161
|
+
// Generate the upload field name: columnName + 'Upload'
|
|
162
|
+
const baseFieldName = build.inflection.attribute({
|
|
163
|
+
codec: pgCodec,
|
|
164
|
+
attributeName
|
|
165
|
+
});
|
|
166
|
+
const uploadFieldName = baseFieldName + 'Upload';
|
|
167
|
+
if (newFields[uploadFieldName]) {
|
|
168
|
+
throw new Error(`Two columns produce the same upload field name '${uploadFieldName}' ` +
|
|
169
|
+
`on codec '${pgCodec.name}'; one of them is '${attributeName}'`);
|
|
146
170
|
}
|
|
171
|
+
newFields = build.extend(newFields, {
|
|
172
|
+
[uploadFieldName]: context.fieldWithHooks({ fieldName: uploadFieldName, isPgUploadField: true }, {
|
|
173
|
+
description: attribute.description
|
|
174
|
+
? `Upload for ${attribute.description}`
|
|
175
|
+
: `File upload for the \`${baseFieldName}\` field.`,
|
|
176
|
+
type: UploadType
|
|
177
|
+
})
|
|
178
|
+
}, `UploadPlugin adding upload field '${uploadFieldName}' for ` +
|
|
179
|
+
`attribute '${attributeName}' on '${pgCodec.name}'`);
|
|
180
|
+
}
|
|
181
|
+
return newFields;
|
|
182
|
+
},
|
|
183
|
+
// Wrap mutation field resolvers to process file uploads
|
|
184
|
+
GraphQLObjectType_fields_field(field, build, context) {
|
|
185
|
+
const { scope: { isRootMutation, fieldName, pgCodec } } = context;
|
|
186
|
+
if (!isRootMutation || !pgCodec || !pgCodec.attributes) {
|
|
187
|
+
return field;
|
|
147
188
|
}
|
|
189
|
+
// Build the mapping of upload field names to their resolvers
|
|
190
|
+
const uploadResolversByFieldName = {};
|
|
191
|
+
const tags = {};
|
|
192
|
+
const types = {};
|
|
193
|
+
const originals = {};
|
|
194
|
+
for (const [attributeName, attribute] of Object.entries(pgCodec.attributes)) {
|
|
195
|
+
const matchedDef = relevantUploadType(attribute, uploadFieldDefinitions);
|
|
196
|
+
if (!matchedDef)
|
|
197
|
+
continue;
|
|
198
|
+
const baseFieldName = build.inflection.attribute({
|
|
199
|
+
codec: pgCodec,
|
|
200
|
+
attributeName
|
|
201
|
+
});
|
|
202
|
+
const uploadFieldName = baseFieldName + 'Upload';
|
|
203
|
+
uploadResolversByFieldName[uploadFieldName] = matchedDef.resolve;
|
|
204
|
+
tags[uploadFieldName] = attribute.extensions?.tags || {};
|
|
205
|
+
types[uploadFieldName] = matchedDef.type || '';
|
|
206
|
+
originals[uploadFieldName] = baseFieldName;
|
|
207
|
+
}
|
|
208
|
+
// If no upload fields match this mutation's codec, skip wrapping
|
|
209
|
+
if (Object.keys(uploadResolversByFieldName).length === 0) {
|
|
210
|
+
return field;
|
|
211
|
+
}
|
|
212
|
+
const defaultResolver = (obj) => obj[fieldName];
|
|
213
|
+
const { resolve: oldResolve = defaultResolver, ...rest } = field;
|
|
214
|
+
return {
|
|
215
|
+
...rest,
|
|
216
|
+
async resolve(source, args, context, info) {
|
|
217
|
+
// Recursively walk args to find and resolve upload thenables.
|
|
218
|
+
async function resolvePromises(obj) {
|
|
219
|
+
const pending = [];
|
|
220
|
+
for (const key of Object.keys(obj)) {
|
|
221
|
+
const value = obj[key];
|
|
222
|
+
if (isThenable(value)) {
|
|
223
|
+
const resolver = uploadResolversByFieldName[key];
|
|
224
|
+
if (!resolver)
|
|
225
|
+
continue;
|
|
226
|
+
pending.push((async () => {
|
|
227
|
+
const upload = await value;
|
|
228
|
+
const uploadForResolver = wrapUploadForMaxFileSize(upload, maxFileSize);
|
|
229
|
+
obj[originals[key]] = await resolver(uploadForResolver, args, context, {
|
|
230
|
+
...info,
|
|
231
|
+
uploadPlugin: {
|
|
232
|
+
tags: tags[key],
|
|
233
|
+
type: types[key]
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// Remove consumed `*Upload` key so downstream resolvers only
|
|
237
|
+
// see the materialized column value.
|
|
238
|
+
delete obj[key];
|
|
239
|
+
})());
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (value !== null && typeof value === 'object') {
|
|
243
|
+
pending.push(resolvePromises(value));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (pending.length > 0) {
|
|
247
|
+
await Promise.all(pending);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await resolvePromises(args);
|
|
251
|
+
return oldResolve(source, args, context, info);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
148
254
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
};
|
|
158
|
-
export default UploadPostGraphilePlugin;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export const UploadPlugin = createUploadPlugin;
|
|
260
|
+
export default UploadPlugin;
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 Upload Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset for including upload support in PostGraphile.
|
|
5
|
+
*/
|
|
6
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
7
|
+
import type { UploadPluginOptions } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Creates a preset that includes the upload plugin with the given options.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { UploadPreset } from 'graphile-upload-plugin';
|
|
14
|
+
*
|
|
15
|
+
* const preset = {
|
|
16
|
+
* extends: [
|
|
17
|
+
* UploadPreset({
|
|
18
|
+
* uploadFieldDefinitions: [
|
|
19
|
+
* {
|
|
20
|
+
* tag: 'upload',
|
|
21
|
+
* resolve: async (upload, args, context, info) => {
|
|
22
|
+
* // Process the upload and return the value to store in the column
|
|
23
|
+
* const stream = upload.createReadStream();
|
|
24
|
+
* const url = await uploadToStorage(stream, upload.filename);
|
|
25
|
+
* return url;
|
|
26
|
+
* },
|
|
27
|
+
* },
|
|
28
|
+
* ],
|
|
29
|
+
* }),
|
|
30
|
+
* ],
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function UploadPreset(options?: UploadPluginOptions): GraphileConfig.Preset;
|
|
35
|
+
export default UploadPreset;
|