Skip to content

Commit

Permalink
Implement TypeScript types via JSDoc comments.
Browse files Browse the repository at this point in the history
Closes #282 .
  • Loading branch information
jaydenseric committed May 6, 2022
1 parent 8d23234 commit d643e7c
Show file tree
Hide file tree
Showing 24 changed files with 578 additions and 649 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"typescript.disableAutomaticTypeAcquisition": true,
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
10 changes: 5 additions & 5 deletions GRAPHQL_MULTIPART_REQUEST_SPEC_URL.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// @ts-check

"use strict";

/**
* [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec)
* URL. Useful for error messages, etc.
* @kind constant
* @name GRAPHQL_MULTIPART_REQUEST_SPEC_URL
* @type {string}
* @ignore
*/
module.exports =
const GRAPHQL_MULTIPART_REQUEST_SPEC_URL =
"https://github.com/jaydenseric/graphql-multipart-request-spec";

module.exports = GRAPHQL_MULTIPART_REQUEST_SPEC_URL;
36 changes: 19 additions & 17 deletions GraphQLUpload.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
// @ts-check

"use strict";

const { GraphQLScalarType, GraphQLError } = require("graphql");
const Upload = require("./Upload.js");

/** @typedef {import("./processRequest").FileUpload} FileUpload */

/**
* A GraphQL `Upload` scalar that can be used in a
* [`GraphQLSchema`](https://graphql.org/graphql-js/type/#graphqlschema).
* It’s value in resolvers is a promise that resolves
* [file upload details]{@link FileUpload} for processing and storage.
* @kind class
* @name GraphQLUpload
* @example <caption>How to `import`.</caption>
* ```js
* import GraphQLUpload from "graphql-upload/GraphQLUpload.js";
* ```
* @example <caption>How to `require`.</caption>
* ```js
* const GraphQLUpload = require("graphql-upload/GraphQLUpload.js");
* ```
* @example <caption>A schema built using [`makeExecutableSchema`](https://www.graphql-tools.com/docs/api/modules/schema#makeexecutableschema) from [`@graphql-tools/schema`](https://npm.im/@graphql-tools/schema).</caption>
* [`GraphQLSchema`](https://graphql.org/graphql-js/type/#graphqlschema). It’s
* value in resolvers is a promise that resolves
* {@link FileUpload file upload details} for processing and storage.
* @example
* A schema built using
* [`makeExecutableSchema`](https://www.graphql-tools.com/docs/api/modules/schema#makeexecutableschema)
* from [`@graphql-tools/schema`](https://npm.im/@graphql-tools/schema):
*
* ```js
* const { makeExecutableSchema } = require("@graphql-tools/schema");
* const GraphQLUpload = require("graphql-upload/GraphQLUpload.js");
*
* const schema = makeExecutableSchema({
* typeDefs: /* GraphQL *\/ `
* typeDefs: `
* scalar Upload
* `,
* resolvers: {
* Upload: GraphQLUpload,
* },
* });
* ```
* @example <caption>A manually constructed schema with an image upload mutation.</caption>
* @example
* A manually constructed schema with an image upload mutation:
*
* ```js
* const { GraphQLSchema, GraphQLObjectType, GraphQLBoolean } = require("graphql");
* const GraphQLUpload = require("graphql-upload/GraphQLUpload.js");
Expand Down Expand Up @@ -62,7 +62,7 @@ const Upload = require("./Upload.js");
* });
* ```
*/
module.exports = new GraphQLScalarType({
const GraphQLUpload = new GraphQLScalarType({
name: "Upload",
description: "The `Upload` scalar type represents a file upload.",
parseValue(value) {
Expand All @@ -76,3 +76,5 @@ module.exports = new GraphQLScalarType({
throw new GraphQLError("Upload serialization unsupported.");
},
});

module.exports = GraphQLUpload;
6 changes: 6 additions & 0 deletions GraphQLUpload.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// @ts-check

import { doesNotThrow, throws } from "assert";
import { parseValue } from "graphql";

import GraphQLUpload from "./GraphQLUpload.js";
import Upload from "./Upload.js";

/**
* Adds `GraphQLUpload` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`GraphQLUpload` scalar `parseValue` with a valid value.", () => {
doesNotThrow(() => {
Expand Down
88 changes: 23 additions & 65 deletions Upload.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,37 @@
// @ts-check

"use strict";

/** @typedef {import("./GraphQLUpload.js")} GraphQLUpload */
/** @typedef {import("./processRequest.js")} processRequest */

/**
* A file expected to be uploaded as it has been declared in the `map` field of
* a [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec).
* The [`processRequest`]{@link processRequest} function places references to an
* instance of this class wherever the file is expected in the
* [GraphQL operation]{@link GraphQLOperation}. The
* [`Upload`]{@link GraphQLUpload} scalar derives it’s value from the
* [`promise`]{@link Upload#promise} property.
* @kind class
* @name Upload
* @example <caption>How to `import`.</caption>
* ```js
* import Upload from "graphql-upload/Upload.js";
* ```
* @example <caption>How to `require`.</caption>
* ```js
* const Upload = require("graphql-upload/Upload.js");
* ```
* A file expected to be uploaded as it was declared in the `map` field of a
* [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec).
* The {@linkcode processRequest} function places references to an instance of
* this class wherever the file is expected in the GraphQL operation. The scalar
* {@linkcode GraphQLUpload} derives it’s value from {@linkcode Upload.promise}.
*/
module.exports = class Upload {
class Upload {
constructor() {
/**
* Promise that resolves file upload details. This should only be utilized
* by [`GraphQLUpload`]{@link GraphQLUpload}.
* @kind member
* @name Upload#promise
* @type {Promise<FileUpload>}
* by {@linkcode GraphQLUpload}.
* @type {Promise<import("./processRequest.js").FileUpload>}
*/
this.promise = new Promise((resolve, reject) => {
/**
* Resolves the upload promise with the file upload details. This should
* only be utilized by [`processRequest`]{@link processRequest}.
* @kind function
* @name Upload#resolve
* @param {FileUpload} file File upload details.
* only be utilized by {@linkcode processRequest}.
* @param {import("./processRequest.js").FileUpload} file File upload
* details.
*/
this.resolve = (file) => {
/**
* The file upload details, available when the
* [upload promise]{@link Upload#promise} resolves. This should only be
* utilized by [`processRequest`]{@link processRequest}.
* @kind member
* @name Upload#file
* @type {undefined|FileUpload}
* {@linkcode Upload.promise} resolves. This should only be utilized by
* {@linkcode processRequest}.
* @type {import("./processRequest.js").FileUpload | undefined}
*/
this.file = file;

Expand All @@ -52,10 +40,8 @@ module.exports = class Upload {

/**
* Rejects the upload promise with an error. This should only be
* utilized by [`processRequest`]{@link processRequest}.
* @kind function
* @name Upload#reject
* @param {object} error Error instance.
* utilized by {@linkcode processRequest}.
* @param {Error} error Error instance.
*/
this.reject = reject;
});
Expand All @@ -64,34 +50,6 @@ module.exports = class Upload {
// https://github.com/nodejs/node/issues/20392
this.promise.catch(() => {});
}
};
}

/**
* File upload details that are only available after the file’s field in the
* [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec)
* has begun streaming in.
* @kind typedef
* @name FileUpload
* @type {object}
* @prop {string} filename File name.
* @prop {string} mimetype File MIME type. Provided by the client and can’t be trusted.
* @prop {string} encoding File stream transfer encoding.
* @prop {FileUploadCreateReadStream} createReadStream Creates a [Node.js readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) of the file’s contents, for processing and storage.
*/

/**
* Creates a
* [Node.js readable stream](https://nodejs.org/api/stream.html#stream_readable_streams)
* of an [uploading file’s]{@link FileUpload} contents, for processing and
* storage. Multiple calls create independent streams. Throws if called after
* all resolvers have resolved, or after an error has interrupted the request.
* @kind typedef
* @name FileUploadCreateReadStream
* @type {Function}
* @param {object} [options] [`fs-capacitor`](https://npm.im/fs-capacitor) [`ReadStreamOptions`](https://github.com/mike-marcacci/fs-capacitor#readstreamoptions).
* @param {string} [options.encoding=null] Specify an encoding for the [`data`](https://nodejs.org/api/stream.html#stream_event_data) chunks to be strings (without splitting multi-byte characters across chunks) instead of Node.js [`Buffer`](https://nodejs.org/api/buffer.html#buffer_buffer) instances. Supported values depend on the [`Buffer` implementation](https://github.com/nodejs/node/blob/v13.7.0/lib/buffer.js#L587-L663) and include `utf8`, `ucs2`, `utf16le`, `latin1`, `ascii`, `base64`, or `hex`.
* @param {number} [options.highWaterMark=16384] Maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource.
* @returns {Readable} [Node.js readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) of the file’s contents.
* @see [Node.js `Readable` stream constructor docs](https://nodejs.org/api/stream.html#stream_new_stream_readable_options).
* @see [Node.js stream backpressure guide](https://nodejs.org/en/docs/guides/backpressuring-in-streams).
*/
module.exports = Upload;
7 changes: 7 additions & 0 deletions Upload.test.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// @ts-check

import { ok, rejects, strictEqual } from "assert";

import Upload from "./Upload.js";

/**
* Adds `Upload` tests.
* @param {import("test-director").default} tests Test director.
*/
export default (tests) => {
tests.add("`Upload` class resolving a file.", async () => {
const upload = new Upload();

ok(upload.promise instanceof Promise);
strictEqual(typeof upload.resolve, "function");

/** @type {any} */
const file = {};

upload.resolve(file);
Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
- Public modules are now individually listed in the package `files` and `exports` fields.
- Removed the package main index module; deep imports must be used.
- Shortened public module deep import paths, removing the `/public/`.
- Implemented TypeScript types via JSDoc comments, closing [#282](https://github.com/jaydenseric/graphql-upload/issues/282).

### Patch

- Updated dev dependencies.
- Simplified dev dependencies and config for ESLint.
- Check TypeScript types via a new package `types` script.
- Removed the [`jsdoc-md`](https://npm.im/jsdoc-md) dev dependency and the related package scripts, replacing the readme “API” section with a manually written “Exports” section.
- Removed the [`hard-rejection`](https://npm.im/hard-rejection) dev dependency. Instead, tests are run with the Node.js CLI flag `--unhandled-rejections=throw` to make Node.js v14 behave like newer versions.
- Removed the [`formdata-node`](https://npm.im/formdata-node) dev dependency. Instead, `File` and `FormData` are imported from [`node-fetch`](https://npm.im/formdata-node).
- Updated GitHub Actions CI config:
Expand All @@ -24,6 +27,7 @@
- Use the `.js` file extension in `require` paths.
- Use the Node.js `Readable` property `readableEncoding` instead of `_readableState.encoding` in tests.
- Fixed a typo in a code comment.
- Updated documentation.

## 13.0.0

Expand Down
70 changes: 41 additions & 29 deletions graphqlUploadExpress.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
// @ts-check

"use strict";

const defaultProcessRequest = require("./processRequest.js");

/**
* Creates [Express](https://expressjs.com) middleware that processes
* Creates [Express](https://expressjs.com) middleware that processes incoming
* [GraphQL multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec)
* using [`processRequest`]{@link processRequest}, ignoring non-multipart
* requests. It sets the request body to be
* [similar to a conventional GraphQL POST request]{@link GraphQLOperation} for
* using {@linkcode processRequest}, ignoring non multipart requests. It sets
* the request `body` to be similar to a conventional GraphQL POST request for
* following GraphQL middleware to consume.
* @kind function
* @name graphqlUploadExpress
* @param {ProcessRequestOptions} options Middleware options. Any [`ProcessRequestOptions`]{@link ProcessRequestOptions} can be used.
* @param {ProcessRequestFunction} [options.processRequest=processRequest] Used to process [GraphQL multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec).
* @returns {Function} Express middleware.
* @example <caption>How to `import`.</caption>
* ```js
* import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.js";
* ```
* @example <caption>How to `require`.</caption>
* ```js
* const graphqlUploadExpress = require("graphql-upload/graphqlUploadExpress.js");
* ```
* @example <caption>Basic [`express-graphql`](https://npm.im/express-graphql) setup.</caption>
* @param {import("./processRequest.js").ProcessRequestOptions & {
* processRequest?: import("./processRequest.js").ProcessRequestFunction
* }} options Options.
* @returns Express middleware.
* @example
* Basic [`express-graphql`](https://npm.im/express-graphql) setup:
*
* ```js
* const express = require("express");
* const graphqlHTTP = require("express-graphql");
Expand All @@ -38,22 +32,36 @@ const defaultProcessRequest = require("./processRequest.js");
* .listen(3000);
* ```
*/
module.exports = function graphqlUploadExpress({
function graphqlUploadExpress({
processRequest = defaultProcessRequest,
...processRequestOptions
} = {}) {
return function graphqlUploadExpressMiddleware(request, response, next) {
/**
* [Express](https://expressjs.com) middleware that processes incoming
* [GraphQL multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec)
* using {@linkcode processRequest}, ignoring non multipart requests. It sets
* the request `body` to be similar to a conventional GraphQL POST request for
* following GraphQL middleware to consume.
* @param {import("express").Request} request
* @param {import("express").Response} response
* @param {import("express").NextFunction} next
*/
function graphqlUploadExpressMiddleware(request, response, next) {
if (!request.is("multipart/form-data")) return next();

const finished = new Promise((resolve) => request.on("end", resolve));
const requestEnd = new Promise((resolve) => request.on("end", resolve));
const { send } = response;

response.send = (...args) => {
finished.then(() => {
response.send = send;
response.send(...args);
});
};
// @ts-ignore Todo: Find a less hacky way to prevent sending a response
// before the request has ended.
response.send =
/** @param {Array<unknown>} args */
(...args) => {
requestEnd.then(() => {
response.send = send;
response.send(...args);
});
};

processRequest(request, response, processRequestOptions)
.then((body) => {
Expand All @@ -64,5 +72,9 @@ module.exports = function graphqlUploadExpress({
if (error.status && error.expose) response.status(error.status);
next(error);
});
};
};
}

return graphqlUploadExpressMiddleware;
}

module.exports = graphqlUploadExpress;
Loading

0 comments on commit d643e7c

Please sign in to comment.