From afbe97dff9df6fffb54e11eac376592a930b48ca Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 31 Jul 2023 17:22:29 +0200 Subject: [PATCH 01/74] Work on 4.0.0 refactor --- .gitattributes | 7 +- .gitignore | 1 + lib/tumblr.js | 510 +++++++++++++++++++-------------- package-lock.json | 145 ++++++---- package.json | 13 +- test/tumblr.test.js | 667 +++++++++++++++++++++++++------------------- tsconfig.json | 3 + tsconfig.lib.json | 17 ++ tsconfig.test.json | 18 ++ 9 files changed, 823 insertions(+), 558 deletions(-) create mode 100644 tsconfig.json create mode 100644 tsconfig.lib.json create mode 100644 tsconfig.test.json diff --git a/.gitattributes b/.gitattributes index 0cbd9132..71ac92c6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ -/gh-pages/ linguist-generated=true +# Generated files +/gh-pages/ linguist-generated +/package-lock.json linguist-generated + +# Syntax highlighting +tsconfig*.json linguist-language=jsonc diff --git a/.gitignore b/.gitignore index 7ecd2cf9..b7e6a4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.bak credentials*.json .nyc_output +*.tsbuildinfo diff --git a/lib/tumblr.js b/lib/tumblr.js index 4677899a..b157db22 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -9,10 +9,7 @@ * @namespace tumblr */ -const qs = require('query-string'); const request = require('request'); -const URL = require('url').URL; - const get = require('lodash/get'); const set = require('lodash/set'); const keys = require('lodash/keys'); @@ -27,7 +24,7 @@ const isArray = require('lodash/isArray'); const isPlainObject = require('lodash/isPlainObject'); const omit = require('lodash/omit'); -const CLIENT_VERSION = '3.0.0'; +const CLIENT_VERSION = '4.0.0-alpha.0'; const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash const API_METHODS = { @@ -39,7 +36,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -55,7 +52,7 @@ const API_METHODS = { * @param {string} blogIdentifier - blog name or URL * @param {number} [size] - avatar size, in pixels * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -70,7 +67,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -85,7 +82,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -101,7 +98,7 @@ const API_METHODS = { * @param {string} blogIdentifier - blog name or URL * @param {string} [type] - filters returned posts to the specified type * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @memberof TumblrClient */ @@ -114,7 +111,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -129,7 +126,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -144,7 +141,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -158,7 +155,7 @@ const API_METHODS = { * @method userInfo * * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -172,7 +169,7 @@ const API_METHODS = { * @method userDashboard * * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -186,7 +183,7 @@ const API_METHODS = { * @method userFollowing * * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -200,7 +197,7 @@ const API_METHODS = { * @method userLikes * * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -215,7 +212,7 @@ const API_METHODS = { * * @param {string} [tag] - tag to search for * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -233,7 +230,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -248,7 +245,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -263,7 +260,7 @@ const API_METHODS = { * * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -279,7 +276,7 @@ const API_METHODS = { * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to delete - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -294,7 +291,7 @@ const API_METHODS = { * * @param {Object} params - parameters sent with the request * @param {Object} params.url - URL of the blog to follow - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -309,7 +306,7 @@ const API_METHODS = { * * @param {Object} params - parameters sent with the request * @param {Object} params.url - URL of the blog to unfollow - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -325,7 +322,7 @@ const API_METHODS = { * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to like * @param {Object} params.reblog_key - Reblog key for the post ID - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -341,7 +338,7 @@ const API_METHODS = { * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to unlike * @param {Object} params.reblog_key - Reblog key for the post ID - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -355,7 +352,7 @@ const API_METHODS = { * Creates a named function with the desired signature * * @param {string} name - function name - * @param {Array} [args] - array of argument names + * @param {Array} args - array of argument names * @param {Function} fn - function that contains the logic that should run * * @return {Function} a named function that takes the desired arguments @@ -363,11 +360,6 @@ const API_METHODS = { * @private */ function createFunction(name, args, fn) { - if (isFunction(args)) { - fn = args; - args = []; - } - return new Function( 'body', 'return function ' + @@ -386,21 +378,19 @@ function createFunction(name, args, fn) { * @return {Function} function that returns a Promise that resolves with the response body or * rejects with the error message * - * @private */ function promisifyRequest(requestMethod) { + /** @this {TumblrClient} */ return function (apiPath, params, callback) { - const promise = new Promise( - function (resolve, reject) { - requestMethod.call(this, apiPath, params, function (err, resp) { - if (err) { - reject(err); - } else { - resolve(resp); - } - }); - }.bind(this), - ); + const promise = new Promise((resolve, reject) => { + requestMethod.call(this, apiPath, params, function (err, resp) { + if (err) { + reject(err); + } else { + resolve(resp); + } + }); + }); if (callback) { promise @@ -419,9 +409,9 @@ function promisifyRequest(requestMethod) { /** * Wraps a function for use as a request callback * - * @param {TumblrClient~callback} callback - function to wrap + * @param {TumblrClientCallback} callback - function to wrap * - * @return {TumblrClient~callback} request callback + * @return {TumblrClientCallback} request callback * * @private */ @@ -456,12 +446,12 @@ function requestCallback(callback) { * Make a get request * * @param {Function} requestGet - function that performs a get request - * @param {Object} credentials - OAuth credentials + * @param {TumblrClient['credentials']} credentials - OAuth credentials * @param {string} baseUrl - base URL for the request * @param {string} apiPath - URL path for the request * @param {Object} requestOptions - additional request options * @param {Object} params - query parameters - * @param {TumblrClient~callback} callback - request callback + * @param {TumblrClientCallback} callback - request callback * * @return {Request} Request object * @@ -470,27 +460,27 @@ function requestCallback(callback) { function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, params, callback) { params = params || {}; - if (credentials.consumer_key) { - params.api_key = credentials.consumer_key; + const url = new URL(apiPath, baseUrl); + if (params) { + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } } - // if the apiPath already has query params, use them - let existingQueryIndex = apiPath.indexOf('?'); - if (existingQueryIndex !== -1) { - let existingParams = qs.parse(apiPath.substr(existingQueryIndex)); - - // extend the existing params with the given params - extend(existingParams, params); - - // reset the given apiPath to remove those query params for clean reassembly - apiPath = apiPath.substring(0, existingQueryIndex); + // Use the `api_key` query param if we don't have full credentials + /** @type {TumblrClient['credentials'] | undefined} */ + let oauth; + if (!credentials.token) { + url.searchParams.set('api_key', credentials.consumer_key); + } else { + oauth = credentials; } return requestGet( extend( { - url: baseUrl + apiPath + '?' + qs.stringify(params), - oauth: credentials, + url: url.toString(), + oauth, json: true, }, requestOptions, @@ -508,7 +498,7 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p * @param {string} apiPath - URL path for the request * @param {Object} requestOptions - additional request options * @param {Object} params - form data - * @param {TumblrClient~callback} callback - request callback + * @param {TumblrClientCallback} callback - request callback * * @return {Request} Request object * @@ -517,12 +507,32 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, params, callback) { params = params || {}; + const url = new URL(apiPath, baseUrl); + + // Move URL search params to send them in the request body + for (const [key, value] of url.searchParams.entries()) { + if (!Object.prototype.hasOwnProperty.call(params, key)) { + params[key] = value; + } + } + // Clear the search params + url.search = ''; + + // Use the `api_key` query param if we don't have full credentials + /** @type {TumblrClient['credentials'] | undefined} */ + let oauth; + if (!credentials.token) { + url.searchParams.set('api_key', credentials.consumer_key); + } else { + oauth = credentials; + } + // Sign without multipart data const currentRequest = requestPost( extend( { - url: baseUrl + apiPath, - oauth: credentials, + url: url.toString(), + oauth, }, requestOptions, ), @@ -541,7 +551,6 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, // Sign it with the non-data parameters const dataKeys = ['data']; currentRequest.form(omit(params, dataKeys)); - currentRequest.oauth(credentials); // Clear the side effects from form(param) delete currentRequest.headers['content-type']; @@ -567,108 +576,6 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, return currentRequest; } -/** - * Adds a request method to the client - * - * @param {Object} client - add the method to this object - * @param {string} methodName - the name of the method - * @param {string} apiPath - the API route, which uses any colon-prefixed segments as arguments - * @param {Array} paramNames - ordered list of required request parameters used as arguments - * @param {String|Function} requestType - the request type or a function that makes the request - * - * @private - */ -function addMethod(client, methodName, apiPath, paramNames, requestType) { - const apiPathSplit = apiPath.split('/'); - const apiPathParamsCount = apiPath.split(/\/:[^/]+/).length - 1; - - const buildApiPath = function (args) { - let pathParamIndex = 0; - return reduce( - apiPathSplit, - function (apiPath, apiPathChunk) { - // Parse arguments in the path - if (apiPathChunk[0] === ':') { - apiPathChunk = args[pathParamIndex++]; - } - - if (apiPathChunk) { - return apiPath + '/' + apiPathChunk; - } else { - return apiPath; - } - }, - '', - ); - }; - - const namedParams = (apiPath.match(/\/:[^/]+/g) || []) - .map(function (param) { - return param.substr(2); - }) - .concat(paramNames, 'params', 'callback'); - - const methodBody = function () { - const argsLength = arguments.length; - const args = new Array(argsLength); - for (let i = 0; i < argsLength; i++) { - args[i] = arguments[i]; - } - - const requiredParamsStart = apiPathParamsCount; - const requiredParamsEnd = requiredParamsStart + paramNames.length; - const requiredParamArgs = args.slice(requiredParamsStart, requiredParamsEnd); - - // Callback is at the end - const callback = isFunction(args[args.length - 1]) ? args.pop() : null; - - // Required Parmas - const params = zipObject(paramNames, requiredParamArgs); - extend(params, isPlainObject(args[args.length - 1]) ? args.pop() : {}); - - // Path arguments are determined after required parameters - const apiPathArgs = args.slice(0, apiPathParamsCount); - - let request = requestType; - if (isString(requestType)) { - request = requestType.toUpperCase() === 'POST' ? client.postRequest : client.getRequest; - } else if (!isFunction(requestType)) { - request = client.getRequest; - } - - return request.call(client, buildApiPath(apiPathArgs), params, callback); - }; - - set(client, methodName, createFunction(methodName, namedParams, methodBody)); -} - -/** - * Adds methods to the client - * - * @param {TumblrClient} client - an instance of the `tumblr.js` API client - * @param {Object} methods - mapping of method names to endpoints. Endpoints can be a string or an - * array of format `[apiPathString, requireParamsArray]` - * @param {String|Function} requestType - the request type or a function that makes the request - * - * @private - */ -function addMethods(client, methods, requestType) { - let apiPath, paramNames; - for (const methodName in methods) { - apiPath = methods[methodName]; - if (isString(apiPath)) { - paramNames = []; - } else if (isPlainObject(apiPath)) { - paramNames = apiPath.paramNames || []; - apiPath = apiPath.path; - } else { - paramNames = apiPath[1] || []; - apiPath = apiPath[0]; - } - addMethod(client, methodName, apiPath, paramNames, requestType || 'GET'); - } -} - /** * Wraps createPost to specify `type` and validate the parameters * @@ -677,9 +584,9 @@ function addMethods(client, methods, requestType) { * * @return {Function} wrapped function * - * @private */ function wrapCreatePost(type, validate) { + /** @this {TumblrClient} */ return function (blogIdentifier, params, callback) { params = extend({ type: type }, params); @@ -709,50 +616,132 @@ function wrapCreatePost(type, validate) { } } - if (arguments.length > 2) { - return this.createPost(blogIdentifier, params, callback); - } else { - return this.createPost(blogIdentifier, params); - } + return this.createPost(blogIdentifier, params, callback); }; } /** * @typedef Options - * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. - * @property {string} [consumer_secret] OAuth1 credential. Required for OAuth endpoints. - * @property {string} [token] OAuth1 credential. Required for OAuth endpoints. - * @property {string} [token_secret] OAuth1 credential. Required for Oauth endpoints. - * @property {string} [baseUrl] (optional) The API url if different from the default. - * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. + * @property {string} [baseUrl] The API url, defaults to "https://api.tumblr.com" + * @property {string} consumer_key + * @property {string} [consumer_secret] + * @property {string} [token] + * @property {string} [token_secret] + * */ class TumblrClient { /** * Creates a Tumblr API client using the given options * - * @param {Options} [options] - client options + * @param {Options} options - client options * * @constructor */ constructor(options) { + /** + * Package version + * @type {typeof CLIENT_VERSION} + */ this.version = CLIENT_VERSION; - this.credentials = omit(options, 'baseUrl', 'request', 'returnPromises'); - this.baseUrl = get(options, 'baseUrl', API_BASE_URL); - - // if someone is providing a custom baseUrl with a path, show a message - // to help them debug if they run into errors. - if (this.baseUrl !== API_BASE_URL && this.baseUrl !== '') { - const baseUrl = new URL(this.baseUrl); - if (baseUrl.pathname !== '/') { - /* eslint-disable no-console */ - console.warn('WARNING! Path detected in your custom baseUrl!'); - console.warn('As of version 3.0.0, tumblr.js no longer includes a path in the baseUrl.'); - console.warn('If you encounter errors, please try to omit the path.'); - /* eslint-enable no-console */ + + try { + const url = new URL(options.baseUrl ?? API_BASE_URL); + + if (url.pathname !== '/') { + throw 'pathname'; + } + + // url.searchParams.size is buggy in node 16, we have to look at keys + if ([...url.searchParams.keys()].length) { + throw 'search'; + } + + if (url.username) { + throw 'username'; + } + + if (url.password) { + throw 'password'; + } + + if (url.hash) { + throw 'hash'; + } + + /** + * Base URL to API requests + * @type {string} + */ + this.baseUrl = url.toString(); + } catch (err) { + switch (err) { + case 'pathname': + throw new TypeError('baseUrl option must not include a pathname.'); + + case 'search': + throw new TypeError('baseUrl option must not include search params (query).'); + + case 'username': + throw new TypeError('baseUrl option must not include username.'); + + case 'password': + throw new TypeError('baseUrl option must not include password.'); + + case 'hash': + throw new TypeError('baseUrl option must not include hash.'); + + default: + throw new TypeError('Invalid baseUrl option provided.'); } } + // A consumer_key is required. It can be used as api_key query param by itself + if (!options.consumer_key || typeof options.consumer_key !== 'string') { + throw new TypeError('You must provide a consumer_key.'); + } + + // All or none of these must be provided + const OAUTH_CREDENTIAL_OPTIONAL_PROPERTIES = /** @type {const} */ ([ + 'consumer_secret', + 'token_secret', + 'token', + ]); + + if ( + OAUTH_CREDENTIAL_OPTIONAL_PROPERTIES.some((propertyName) => + Object.prototype.hasOwnProperty.call(options, propertyName), + ) + ) { + if (!options.consumer_secret || typeof options.consumer_secret !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid consumer_secret provided.`, + ); + } + if (!options.token || typeof options.token !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid token provided.`, + ); + } + if (!options.token_secret || typeof options.token_secret !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid token_secret provided.`, + ); + } + + /** + * @type {{readonly consumer_key: string; readonly consumer_secret?: string; readonly token?: string; readonly token_secret?: string}} + */ + this.credentials = { + consumer_key: options.consumer_key, + consumer_secret: options.consumer_secret, + token: options.token, + token_secret: options.token_secret, + }; + } else { + this.credentials = { consumer_key: options.consumer_key }; + } + this.requestOptions = { followRedirect: false, headers: { @@ -774,7 +763,7 @@ class TumblrClient { * @param {Object} params - parameters sent with the request * @param {string} [params.title] - post title text * @param {string} params.body - post body text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -795,7 +784,7 @@ class TumblrClient { * @param {Stream|Array} params.data - an image or array of images * @param {string} params.data64 - base64-encoded image data * @param {string} [params.caption] - post caption text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -814,7 +803,7 @@ class TumblrClient { * @param {Object} params - parameters sent with the request * @param {string} params.quote - quote text * @param {string} [params.source] - quote source - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -837,7 +826,7 @@ class TumblrClient { * @param {string} [params.excerpt] - an excerpt from the page the link points to * @param {string} [params.author] - the name of the author of the page the link points to * @param {string} [params.description] - post caption text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -856,7 +845,7 @@ class TumblrClient { * @param {Object} params - parameters sent with the request * @param {string} [params.title] - post title text * @param {string} params.conversation - chat text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -876,7 +865,7 @@ class TumblrClient { * @param {string} params.external_url - image source URL * @param {Stream} params.data - an audio file * @param {string} [params.caption] - post caption text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -896,7 +885,7 @@ class TumblrClient { * @param {string} params.embed - embed code or a video URL * @param {Stream} params.data - a video file * @param {string} [params.caption] - post caption text - * @param {TumblrClient~callback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] - invoked when the request completes * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used * @@ -915,15 +904,16 @@ class TumblrClient { * * @param {string} apiPath - URL path for the request * @param {Object} params - query parameters - * @param {TumblrClient~callback} [callback] - request callback + * @param {TumblrClientCallback} callback - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ getRequest(apiPath, params, callback) { - if (isFunction(params)) { + if (typeof params === 'function') { callback = params; params = {}; } + return getRequest( request.get, this.credentials, @@ -934,21 +924,21 @@ class TumblrClient { callback, ); } - /** * Performs a POST request * * @param {string} apiPath - URL path for the request * @param {Object} params - form parameters - * @param {TumblrClient~callback} [callback] - request callback + * @param {TumblrClientCallback} callback - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ postRequest(apiPath, params, callback) { - if (isFunction(params)) { + if (typeof params === 'function') { callback = params; params = {}; } + return postRequest( request.post, this.credentials, @@ -959,7 +949,6 @@ class TumblrClient { callback, ); } - /** * Sets the client to return Promises instead of Request objects by patching the `getRequest` and * `postRequest` methods on the client @@ -968,33 +957,130 @@ class TumblrClient { this.getRequest = promisifyRequest(this.getRequest); this.postRequest = promisifyRequest(this.postRequest); } - /** * Adds GET methods to the client * * @param {Object} methods - mapping of method names to endpoints */ addGetMethods(methods) { - addMethods(this, methods, 'GET'); + this.addMethods(methods, 'GET'); } - /** * Adds POST methods to the client * * @param {Object} methods - mapping of method names to endpoints */ addPostMethods(methods) { - addMethods(this, methods, 'POST'); + this.addMethods(methods, 'POST'); + } + + /** + * Adds methods to the client + * + * @this {TumblrClient} + * @param {Object} methods - mapping of method names to endpoints. Endpoints can be a string or an + * array of format `[apiPathString, requireParamsArray]` + * @param {'GET'|'POST'} [requestType] - the request type or a function that makes the request + * + * @private + */ + addMethods(methods, requestType) { + let apiPath, paramNames; + for (const methodName in methods) { + apiPath = methods[methodName]; + if (isString(apiPath)) { + paramNames = []; + } else if (isPlainObject(apiPath)) { + paramNames = apiPath.paramNames || []; + apiPath = apiPath.path; + } else { + paramNames = apiPath[1] || []; + apiPath = apiPath[0]; + } + this.addMethod(methodName, apiPath, paramNames, requestType); + } + } + + /** + * Adds a request method to the client + * + * @param {string} methodName - the name of the method + * @param {string} apiPath - the API route, which uses any colon-prefixed segments as arguments + * @param {ReadonlyArray} paramNames - ordered list of required request parameters used as arguments + * @param {'GET'|'POST'} [requestType] - the request type or a function that makes the request + * + * @private + */ + addMethod(methodName, apiPath, paramNames, requestType) { + const apiPathSplit = apiPath.split('/'); + const apiPathParamsCount = apiPath.split(/\/:[^/]+/).length - 1; + + const buildApiPath = function (args) { + let pathParamIndex = 0; + return reduce( + apiPathSplit, + function (apiPath, apiPathChunk) { + // Parse arguments in the path + if (apiPathChunk[0] === ':') { + apiPathChunk = args[pathParamIndex++]; + } + + if (apiPathChunk) { + return apiPath + '/' + apiPathChunk; + } else { + return apiPath; + } + }, + '', + ); + }; + + const namedParams = (apiPath.match(/\/:[^/]+/g) || []) + .map(function (param) { + return param.substr(2); + }) + .concat(paramNames, 'params', 'callback'); + + const methodBody = + /** @this {TumblrClient} */ + function () { + const argsLength = arguments.length; + const args = new Array(argsLength); + for (let i = 0; i < argsLength; i++) { + args[i] = arguments[i]; + } + + const requiredParamsStart = apiPathParamsCount; + const requiredParamsEnd = requiredParamsStart + paramNames.length; + const requiredParamArgs = args.slice(requiredParamsStart, requiredParamsEnd); + + // Callback is at the end + const callback = isFunction(args[args.length - 1]) ? args.pop() : null; + + // Required Parmas + const params = zipObject(paramNames, requiredParamArgs); + extend(params, isPlainObject(args[args.length - 1]) ? args.pop() : {}); + + // Path arguments are determined after required parameters + const apiPathArgs = args.slice(0, apiPathParamsCount); + + const requestMethod = requestType?.toUpperCase() === 'POST' ? 'postRequest' : 'getRequest'; + + return this[requestMethod](buildApiPath(apiPathArgs), params, callback); + }.bind(this); + + set(this, methodName, createFunction(methodName, namedParams, methodBody)); } } /** * Handles the response from a client reuest * - * @callback TumblrClient~callback + * @callback TumblrClientCallback * @param {?Error} err - error message * @param {?Object} resp - response body * @param {?string} [response] - raw response + * @returns {void} */ /* @@ -1012,7 +1098,7 @@ module.exports = { /** * Creates a Tumblr Client * - * @param {Options} [options] - client options + * @param {Options} options - client options * * @return {TumblrClient} {@link TumblrClient} instance * diff --git a/package-lock.json b/package-lock.json index 3502c08d..7851d008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,25 @@ { "name": "tumblr.js", - "version": "3.0.0", + "version": "4.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tumblr.js", - "version": "3.0.0", + "version": "4.0.0-alpha.0", "license": "Apache-2.0", "dependencies": { + "@types/lodash": "^4.0.0", + "@types/node": ">=16", + "@types/request": "^2.0.0", "lodash": "^4.17.11", - "query-string": "^6.1.0", "request": "^2.88.2" }, "devDependencies": { + "@tsconfig/node16": "^16.1.0", + "@tsconfig/strictest": "^2.0.1", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", "chai": "^4.1.2", "docdash": "^2.0.1", "eslint": "^8.45.0", @@ -23,7 +29,8 @@ "mocha": "^10.2.0", "nock": "^13.3.2", "nyc": "^15.1.0", - "prettier": "^3.0.0" + "prettier": "^3.0.0", + "typescript": "^5.1.6" }, "engines": { "node": ">=16", @@ -649,12 +656,40 @@ "node": ">= 8" } }, + "node_modules/@tsconfig/node16": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.0.tgz", + "integrity": "sha512-cfwhqrdZEKS+Iqu1OPDwmKsOV/eo7q4sPhWzOXc1rU77nnPFV3+77yPg8uKQ2e8eir6mERCvrKnd+EGa4qo4bQ==", + "dev": true + }, + "node_modules/@tsconfig/strictest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/strictest/-/strictest-2.0.1.tgz", + "integrity": "sha512-7JHHCbyCsGUxLd0pDbp24yz3zjxw2t673W5oAP6HCEdr/UUhaRhYd3SSnUsGCk+VnPVJVA4mXROzbhI+nyIk+w==", + "dev": true + }, + "node_modules/@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, "node_modules/@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.196", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", + "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==" + }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", @@ -671,6 +706,46 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.39.tgz", + "integrity": "sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ==" + }, + "node_modules/@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1190,14 +1265,6 @@ "node": ">=0.10.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", @@ -1648,14 +1715,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -3334,23 +3393,6 @@ "node": ">=0.6" } }, - "node_modules/query-string": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", - "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", - "dependencies": { - "decode-uri-component": "^0.2.0", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3603,14 +3645,6 @@ "node": ">=8" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "engines": { - "node": ">=6" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3641,14 +3675,6 @@ "node": ">=0.10.0" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3819,6 +3845,19 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", diff --git a/package.json b/package.json index 8f635a07..43b84dd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tumblr.js", - "version": "3.0.0", + "version": "4.0.0-alpha.0", "description": "Official JavaScript client for the Tumblr API", "main": "./lib/tumblr", "type": "commonjs", @@ -47,11 +47,17 @@ } ], "dependencies": { + "@types/lodash": "^4.0.0", + "@types/node": ">=16", + "@types/request": "^2.0.0", "lodash": "^4.17.11", - "query-string": "^6.1.0", "request": "^2.88.2" }, "devDependencies": { + "@tsconfig/node16": "^16.1.0", + "@tsconfig/strictest": "^2.0.1", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", "chai": "^4.1.2", "docdash": "^2.0.1", "eslint": "^8.45.0", @@ -61,7 +67,8 @@ "mocha": "^10.2.0", "nock": "^13.3.2", "nyc": "^15.1.0", - "prettier": "^3.0.0" + "prettier": "^3.0.0", + "typescript": "^5.1.6" }, "files": [ "/lib", diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 3675c5f5..e044bb76 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -1,10 +1,11 @@ +require('mocha'); + const fs = require('fs'); const path = require('path'); +// @ts-expect-error +const Request = require('request').Request; const JSON5 = require('json5'); -const qs = require('query-string'); -const forEach = require('lodash/forEach'); -const lowerCase = require('lodash/lowerCase'); const assert = require('chai').assert; const nock = require('nock'); @@ -17,14 +18,10 @@ const DUMMY_CREDENTIALS = { token: 'Toad', token_secret: 'Princess Toadstool', }; -const DUMMY_API_URL = 'https://t.umblr.com'; -const URL_PARAM_REGEX = /\/:([^/]+)/g; +const DUMMY_API_URL = 'https://example.com'; -function createQueryString(obj) { - const queryString = qs.stringify(obj); - return queryString ? '?' + queryString : ''; -} +const URL_PARAM_REGEX = /\/:([^/]+)/g; describe('tumblr.js', function () { it('can be included without throwing', function () { @@ -38,29 +35,66 @@ describe('tumblr.js', function () { it('creates a TumblrClient instance', function () { assert.isFunction(tumblr.createClient); - const client = tumblr.createClient(); + const client = tumblr.createClient({ consumer_key: 'abc123' }); assert.isTrue(client instanceof tumblr.Client); + assert.equal(client.credentials.consumer_key, 'abc123'); }); it('passes credentials to the client', function () { - const client = tumblr.createClient(DUMMY_CREDENTIALS); - assert.equal(client.credentials.consumer_key, DUMMY_CREDENTIALS.consumer_key); - assert.equal(client.credentials.consumer_secret, DUMMY_CREDENTIALS.consumer_secret); - assert.equal(client.credentials.token, DUMMY_CREDENTIALS.token); - assert.equal(client.credentials.token_secret, DUMMY_CREDENTIALS.token_secret); + const credentials = DUMMY_CREDENTIALS; + + const client = tumblr.createClient(credentials); + assert.equal(client.credentials.consumer_key, credentials.consumer_key); + assert.equal(client.credentials.consumer_secret, credentials.consumer_secret); + assert.equal(client.credentials.token, credentials.token); + assert.equal(client.credentials.token_secret, credentials.token_secret); }); it('passes baseUrl to the client', function () { - const baseUrl = 'https://t.umblr.com/v2'; + const baseUrl = 'https://example.com/'; + assert.equal(tumblr.createClient({ consumer_key: 'abc123', baseUrl }).baseUrl, baseUrl); + + const baseUrlNoSlash = 'https://example.com'; + assert.equal( + tumblr.createClient({ consumer_key: 'abc123', baseUrl: baseUrlNoSlash }).baseUrl, + baseUrl, + ); + }); + + it('throws on baseUrl with path', function () { + assert.throws( + () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/v2' }), + 'baseUrl option must not include a pathname.', + ); + }); - const client = tumblr.createClient({ baseUrl: baseUrl }); - assert.equal(client.baseUrl, baseUrl); + it('throws on baseUrl with search', function () { + assert.throws( + () => + tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/?params' }), + 'baseUrl option must not include search params (query).', + ); }); - it('passes returnPromises to the client', function () { - const client = tumblr.createClient({ returnPromises: true }); - assert.notEqual(client.getRequest, tumblr.Client.prototype.getRequest); - assert.notEqual(client.postRequest, tumblr.Client.prototype.postRequest); + it('throws on baseUrl with username', function () { + assert.throws( + () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://user@example.com/' }), + 'baseUrl option must not include username.', + ); + }); + + it('throws on baseUrl with password', function () { + assert.throws( + () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://:pw@example.com/' }), + 'baseUrl option must not include password.', + ); + }); + + it('throws on baseUrl with hash', function () { + assert.throws( + () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/#hash' }), + 'baseUrl option must not include hash.', + ); }); }); @@ -70,38 +104,33 @@ describe('tumblr.js', function () { describe('constructor', function () { it('creates a TumblrClient instance', function () { - const client = new TumblrClient(); + const client = new TumblrClient(DUMMY_CREDENTIALS); assert.isTrue(client instanceof TumblrClient); }); it('uses the supplied credentials', function () { - const client = new TumblrClient(DUMMY_CREDENTIALS); - assert.equal(client.credentials.consumer_key, DUMMY_CREDENTIALS.consumer_key); - assert.equal(client.credentials.consumer_secret, DUMMY_CREDENTIALS.consumer_secret); - assert.equal(client.credentials.token, DUMMY_CREDENTIALS.token); - assert.equal(client.credentials.token_secret, DUMMY_CREDENTIALS.token_secret); - }); + const credentials = DUMMY_CREDENTIALS; - it('uses the supplied baseUrl', function () { - const baseUrl = DUMMY_API_URL; - const client = tumblr.createClient({ baseUrl: baseUrl }); - assert.equal(client.baseUrl, baseUrl); + const client = new TumblrClient(credentials); + assert.equal(client.credentials.consumer_key, credentials.consumer_key); + assert.equal(client.credentials.consumer_secret, credentials.consumer_secret); + assert.equal(client.credentials.token, credentials.token); + assert.equal(client.credentials.token_secret, credentials.token_secret); }); - it('uses the supplied returnPromises value', function () { - const client = tumblr.createClient({ returnPromises: true }); - assert.notEqual(client.getRequest, tumblr.Client.prototype.getRequest); - assert.notEqual(client.postRequest, tumblr.Client.prototype.postRequest); + it('uses the supplied baseUrl', function () { + const client = tumblr.createClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL }); + assert.equal(client.baseUrl, DUMMY_API_URL.replace(/\/?$/, '/')); }); describe('default options', function () { it('uses the default Tumblr API base URL', function () { - const client = tumblr.createClient(); - assert.equal(client.baseUrl, 'https://api.tumblr.com'); + const client = tumblr.createClient(DUMMY_CREDENTIALS); + assert.equal(client.baseUrl, 'https://api.tumblr.com/'); }); it('does not return Promises', function () { - const client = tumblr.createClient(); + const client = tumblr.createClient(DUMMY_CREDENTIALS); assert.equal(client.getRequest, tumblr.Client.prototype.getRequest); assert.equal(client.postRequest, tumblr.Client.prototype.postRequest); }); @@ -110,7 +139,7 @@ describe('tumblr.js', function () { describe('#returnPromises', function () { it('modifies getRequest and postRequest', function () { - const client = new TumblrClient(); + const client = new TumblrClient(DUMMY_CREDENTIALS); const getRequestBefore = client.getRequest; const postRequestBefore = client.postRequest; client.returnPromises(); @@ -119,12 +148,12 @@ describe('tumblr.js', function () { }); }); + /** @type {import('../lib/tumblr.js').Client} */ let client; beforeEach(function () { client = new TumblrClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL, - returnPromises: false, }); }); @@ -135,7 +164,7 @@ describe('tumblr.js', function () { */ describe('default methods', function () { - const defaulthMethods = [ + /** @type {const} */ ([ 'blogInfo', 'blogAvatar', 'blogLikes', @@ -164,9 +193,7 @@ describe('tumblr.js', function () { 'createChatPost', 'createAudioPost', 'createVideoPost', - ]; - - forEach(defaulthMethods, function (methodName) { + ]).forEach(function (methodName) { it('has #' + methodName, function () { assert.isFunction(client[methodName]); }); @@ -182,25 +209,14 @@ describe('tumblr.js', function () { * - TumblrClient#postRequest */ + /** + * @param {'get'|'post'} httpMethod + * @param {any} data + * @param {string} apiPath + */ function setupNockBeforeAfter(httpMethod, data, apiPath) { - let queryParams, testApiPath; - before(function () { - queryParams = {}; - - if (client.credentials.consumer_key) { - queryParams.api_key = client.credentials.consumer_key; - } - - testApiPath = apiPath; - if (httpMethod === 'get') { - testApiPath += createQueryString(queryParams); - } - - nock(client.baseUrl) - .persist() - [httpMethod](testApiPath) - .reply(data.body.meta.status, data.body); + nock(client.baseUrl)[httpMethod](apiPath).reply(data.body.meta.status, data.body).persist(); }); after(function () { @@ -208,76 +224,235 @@ describe('tumblr.js', function () { }); } - forEach( - { - get: 'getRequest', - post: 'postRequest', - }, - function (clientMethod, httpMethod) { - describe('#' + clientMethod, function () { - const fixtures = JSON5.parse( - fs.readFileSync(path.join(__dirname, 'fixtures/' + httpMethod + '.json5')).toString(), - ); - - /** - * ### Callback - */ - - describe('returnPromises disabled', function () { - forEach(fixtures, function (data, apiPath) { - describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - - setupNockBeforeAfter(httpMethod, data, apiPath); - - describe('params and callback', function () { - before(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; + it('get request expected headers', async () => { + client.returnPromises(); + const scope = nock(client.baseUrl, { + reqheaders: { + 'user-agent': `tumblr.js/${client.version}`, + accept: 'application/json', + authorization: (value) => { + return [ + value.startsWith('OAuth '), + value.includes('oauth_signature_method="HMAC-SHA1"'), + value.includes('oauth_version="1.0"'), + value.includes(`oauth_consumer_key="${DUMMY_CREDENTIALS.consumer_key}"`), + value.includes(`oauth_token="${DUMMY_CREDENTIALS.token}"`), + /oauth_nonce="[^"]+"/.test(value), + /oauth_timestamp="[^"]+"/.test(value), + /oauth_signature="[^"]+"/.test(value), + ].every((passes) => passes); + }, + }, + }) + .get('/') + .reply(200, { meta: {}, response: {} }); + + // @ts-expect-error Promise request with no params, this is OK. + assert.isOk(await client.getRequest('/')); + scope.done(); + }); + + it('get request sends api_key when all creds are not provided', async () => { + const client = new TumblrClient({ consumer_key: 'abc123' }); + client.returnPromises(); + const scope = nock(client.baseUrl, { + badheaders: ['authorization'], + }) + .get('/') + .query({ api_key: 'abc123' }) + .reply(200, { meta: {}, response: {} }); + + // @ts-expect-error Promise request with no params, this is OK. + assert.isOk(await client.getRequest('/')); + scope.done(); + }); - returnValue = client[clientMethod](apiPath, params, function () { - callback.apply(this, arguments); + it('post request expected headers', async () => { + client.returnPromises(); + const scope = nock(client.baseUrl, { + reqheaders: { + 'user-agent': `tumblr.js/${client.version}`, + 'content-type': /^multipart\/form-data; ?boundary=-*\d+/, + 'content-length': /^\d+/, + authorization: (value) => { + return [ + value.startsWith('OAuth '), + value.includes('oauth_signature_method="HMAC-SHA1"'), + value.includes('oauth_version="1.0"'), + value.includes(`oauth_consumer_key="${DUMMY_CREDENTIALS.consumer_key}"`), + value.includes(`oauth_token="${DUMMY_CREDENTIALS.token}"`), + /oauth_nonce="[^"]+"/.test(value), + /oauth_timestamp="[^"]+"/.test(value), + /oauth_signature="[^"]+"/.test(value), + ].every((passes) => passes); + }, + }, + }) + .post('/') + .reply(200, { meta: {}, response: {} }); + + // @ts-expect-error Promise request with no params, this is OK. + assert.isOk(await client.postRequest('/')); + scope.done(); + }); + + it('get request sends api_key when all creds are not provided', async () => { + const client = new TumblrClient({ consumer_key: 'abc123' }); + client.returnPromises(); + const scope = nock(client.baseUrl, { + badheaders: ['authorization'], + }) + .post('/') + .query({ api_key: 'abc123' }) + .reply(200, { meta: {}, response: {} }); + + // @ts-expect-error Promise request with no params, this is OK. + assert.isOk(await client.postRequest('/')); + scope.done(); + }); + + /** @type {const} */ ([ + ['get', 'getRequest'], + ['post', 'postRequest'], + ]).forEach(function ([httpMethod, clientMethod]) { + describe('#' + clientMethod, function () { + const fixtures = JSON5.parse( + fs.readFileSync(path.join(__dirname, 'fixtures/' + httpMethod + '.json5')).toString(), + ); + + /** + * ### Callback + */ + + describe('returnPromises disabled', function () { + Object.entries(fixtures).forEach(function ([apiPath, data]) { + describe(apiPath, function () { + let callbackInvoked, requestError, requestResponse, returnValue; + const params = {}; + + const callback = function (err, resp) { + callbackInvoked = true; + requestError = err; + requestResponse = resp; + }; + + setupNockBeforeAfter(httpMethod, data, apiPath); + + describe('params and callback', function () { + before(function (done) { + callbackInvoked = false; + requestError = false; + requestResponse = false; + + returnValue = client[clientMethod]( + apiPath, + params, + /** @param {any} args */ + function (...args) { + callback.call(client, ...args); done(); - }); + }, + ); + }); + + if (httpMethod === 'post') { + // Nock seems to cause the POST request to return a Promise, + // making this difficult to properly test. + it('returns a Request'); + } else { + it('returns a Request', function () { + assert.isTrue(returnValue instanceof Request); }); + } - if (httpMethod === 'post') { - // Nock seems to cause the POST request to return a Promise, - // making this difficult to properly test. - it('returns a Request'); - } else { - it('returns a Request', function () { - assert.isTrue(returnValue instanceof require('request').Request); - }); - } + it('invokes the callback', function () { + assert.isTrue(callbackInvoked); + }); - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); + it('gets a successful response', function () { + assert.isNull(requestError, 'err is falsy'); + assert.isDefined(requestResponse); + }); + }); - it('gets a successful response', function () { - assert.isNotOk(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); + describe('callback only', function () { + before(function (done) { + callbackInvoked = false; + requestError = false; + requestResponse = false; + + // @ts-expect-error This is a bad function signature - optionals in middle @TODO + client[clientMethod]( + apiPath, + /** @param {any} args */ + function (...args) { + callback.call(client, ...args); + done(); + }, + ); }); - describe('callback only', function () { - before(function (done) { + it('invokes the callback', function () { + assert.isTrue(callbackInvoked); + }); + + it('gets a successful response', function () { + assert.isNull(requestError, 'err is falsy'); + assert.isDefined(requestResponse); + }); + }); + }); + }); + }); + + /** + * ### Promises + */ + + describe('returnPromises enabled', function () { + beforeEach(function () { + client.returnPromises(); + }); + + /** @type {const} */ ([ + ['get', 'getRequest'], + ['post', 'postRequest'], + ]).forEach(function ([httpMethod, clientMethod]) { + describe('#' + clientMethod, function () { + Object.entries(fixtures).forEach(function ([apiPath, data]) { + describe(apiPath, function () { + let callbackInvoked, requestError, requestResponse, returnValue; + const params = {}; + const callback = function (err, resp) { + callbackInvoked = true; + requestError = err; + requestResponse = resp; + }; + + setupNockBeforeAfter(httpMethod, data, apiPath); + + beforeEach(function (done) { callbackInvoked = false; requestError = false; requestResponse = false; - client[clientMethod](apiPath, function () { - callback.apply(this, arguments); - done(); - }); + // @ts-expect-error It's promises, no callback + returnValue = client[clientMethod](apiPath, params); + // Invoke the callback when the Promise resolves or rejects + returnValue.then( + function (resp) { + callback(null, resp); + done(); + }, + function (err) { + callback(err, null); + done(); + }, + ); + }); + + it('returns a Promise', function () { + assert.isTrue(returnValue instanceof Promise); }); it('invokes the callback', function () { @@ -285,87 +460,16 @@ describe('tumblr.js', function () { }); it('gets a successful response', function () { - assert.isNotOk(requestError, 'err is falsy'); + assert.isNull(requestError, 'err is falsy'); assert.isDefined(requestResponse); }); }); }); }); }); - - /** - * ### Promises - */ - - describe('returnPromises enabled', function () { - beforeEach(function () { - client.returnPromises(); - }); - - forEach( - { - get: 'getRequest', - post: 'postRequest', - }, - function (clientMethod, httpMethod) { - describe('#' + clientMethod, function () { - const fixtures = JSON5.parse( - fs - .readFileSync(path.join(__dirname, 'fixtures/' + httpMethod + '.json5')) - .toString(), - ); - - forEach(fixtures, function (data, apiPath) { - describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - - setupNockBeforeAfter(httpMethod, data, apiPath); - - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - returnValue = client[clientMethod](apiPath, params); - // Invoke the callback when the Promise resolves or rejects - returnValue - .then(function (resp) { - callback(null, resp); - done(); - }) - .catch(function (err) { - callback(err, null); - done(); - }); - }); - - it('returns a Promise', function () { - assert.isTrue(returnValue instanceof Promise); - }); - - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNotOk(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); - }); - }); - }); - }, - ); - }); }); - }, - ); + }); + }); /** * ## Request methods @@ -376,116 +480,101 @@ describe('tumblr.js', function () { * - TumblrClient#addPostMethods */ - forEach( - { - get: 'addGetMethods', - post: 'addPostMethods', - }, - function (clientMethod, httpMethod) { - describe('#' + clientMethod, function () { - const data = { - meta: { - status: 200, - msg: 'k', + /** @type {const} */ ([ + ['get', 'addGetMethods'], + ['post', 'addPostMethods'], + ]).forEach(function ([httpMethod, clientMethod]) { + describe('#' + clientMethod, function () { + const data = { + meta: { + status: 200, + msg: 'k', + }, + body: { + response: { + ayy: 'lmao', }, - body: { - response: { - ayy: 'lmao', - }, - }, - }; - - const addMethods = { - testNoPathParameters: '/no/params', - testOnePathParameter: '/one/:url/param', - testTwoPathParameters: '/one/:url/param', - testRequiredParams: ['/quert/params', ['id']], - testPathAndRequiredParams: ['/query/:url/params', ['id']], - }; - - beforeEach(function () { - client[clientMethod](addMethods); + }, + }; + + const addMethods = + /** @type {Record]>} */ ({ + noPathParameters: ['/no/params', []], + onePathParameter: ['/one/:url/param', []], + twoPathParameters: ['/one/:url/param', []], + requiredParams: ['/query/params', ['id']], + pathAndRequiredParams: ['/query/:url/params', ['id']], }); - forEach(addMethods, function (apiPath, methodName) { - describe(lowerCase(methodName).replace(/^test /i, ''), function () { - let callbackInvoked, requestError, requestResponse; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - const queryParams = {}; - const args = []; + beforeEach(function () { + client[clientMethod](addMethods); + }); - if (typeof apiPath === 'string') { - forEach(apiPath.match(URL_PARAM_REGEX), function (apiPathParam) { - args.push(apiPathParam.replace(URL_PARAM_REGEX, '$1')); - }); - apiPath = apiPath.replace(URL_PARAM_REGEX, '/$1'); - } else { - forEach(apiPath[0].match(URL_PARAM_REGEX), function (apiPathParam) { - args.push(apiPathParam.replace(URL_PARAM_REGEX, '$1')); - }); - forEach(apiPath[1], function (param) { - queryParams[param] = param + ' value'; - args.push(queryParams[param]); - }); - apiPath = apiPath[0].replace(URL_PARAM_REGEX, '/$1'); - } + Object.entries(addMethods).forEach(function ([methodName, [apiPath, params]]) { + describe(methodName, function () { + let callbackInvoked, requestError, requestResponse; + const callback = function (err, resp) { + callbackInvoked = true; + requestError = err; + requestResponse = resp; + }; + const queryParams = {}; + const args = []; + + apiPath.match(URL_PARAM_REGEX)?.forEach(function (apiPathParam) { + args.push(apiPathParam.replace(URL_PARAM_REGEX, '$1')); + }); + params.forEach(function (param) { + queryParams[param] = param + ' value'; + args.push(queryParams[param]); + }); + apiPath = apiPath.replace(URL_PARAM_REGEX, '/$1'); - args.push(params); + beforeEach(function (done) { + callbackInvoked = false; + requestError = false; + requestResponse = false; - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; + if (client.credentials.consumer_key) { + queryParams.api_key = client.credentials.consumer_key; + } - if (client.credentials.consumer_key) { - queryParams.api_key = client.credentials.consumer_key; - } + const scope = nock(client.baseUrl)[httpMethod](apiPath); + if (params.length) { + scope.query(true); + } - let testApiPath = apiPath; - if (httpMethod === 'get') { - testApiPath += createQueryString(queryParams); - } + scope.reply(data.meta.status, data.body).persist(); - nock(client.baseUrl) - .persist() - [httpMethod](testApiPath) - .reply(data.meta.status, data.body); - - return client[methodName].apply( - client, - args.concat(function () { - callback.apply(this, arguments); - done(); - }), - ); - }); + return client[methodName].apply( + client, + args.concat(function (...args) { + callback.call(client, ...args); + done(); + }), + ); + }); - afterEach(function () { - nock.cleanAll(); - }); + afterEach(function () { + nock.cleanAll(); + }); - it('method is a function', function () { - assert.isFunction(client[methodName]); - }); + it('method is a function', function () { + assert.isFunction(client[methodName]); + }); - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); + it('invokes the callback', function () { + assert.isTrue(callbackInvoked); + }); - it('gets a successful response', function () { - assert.isNotOk(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); + it('gets a successful response', function () { + assert.isNull(requestError, 'err is falsy'); + assert.isDefined(requestResponse); }); }); }); - }, - ); + }); + }); /** * ~fin~ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..60e5a05f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "references": [{ "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.test.json" }] +} diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 00000000..8653dc53 --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node16/tsconfig.json"], + "compilerOptions": { + "composite": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "lib", + "moduleResolution": "node16", + "types": ["node"], + + "noUncheckedIndexedAccess": false, + + // @todo Work through these + "noImplicitAny": false + }, + "files": ["lib/tumblr.js"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..7e23586c --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node16/tsconfig.json"], + "compilerOptions": { + "composite": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": ".", + "moduleResolution": "node16", + "types": ["node", "mocha"], + + "noUncheckedIndexedAccess": false, + + // @todo Work through these + "noImplicitAny": false + }, + "include": ["test/**/*", "integration/**/*"], + "references": [{ "path": "./tsconfig.lib.json" }] +} From 4db427c0894ed543a16dac31fd2487a4a63ec64e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:24:27 +0200 Subject: [PATCH 02/74] Improve tests --- test/tumblr.test.js | 158 ++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 93 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index e044bb76..72a09d00 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -1,4 +1,4 @@ -require('mocha'); +const tumblr = require('../lib/tumblr.js'); const fs = require('fs'); const path = require('path'); @@ -24,118 +24,90 @@ const DUMMY_API_URL = 'https://example.com'; const URL_PARAM_REGEX = /\/:([^/]+)/g; describe('tumblr.js', function () { - it('can be included without throwing', function () { - assert.doesNotThrow(function () { - require('../lib/tumblr.js'); - }); - }); - - describe('createClient', function () { - const tumblr = require('../lib/tumblr.js'); - - it('creates a TumblrClient instance', function () { - assert.isFunction(tumblr.createClient); - const client = tumblr.createClient({ consumer_key: 'abc123' }); - assert.isTrue(client instanceof tumblr.Client); - assert.equal(client.credentials.consumer_key, 'abc123'); - }); - - it('passes credentials to the client', function () { - const credentials = DUMMY_CREDENTIALS; - - const client = tumblr.createClient(credentials); - assert.equal(client.credentials.consumer_key, credentials.consumer_key); - assert.equal(client.credentials.consumer_secret, credentials.consumer_secret); - assert.equal(client.credentials.token, credentials.token); - assert.equal(client.credentials.token_secret, credentials.token_secret); - }); - - it('passes baseUrl to the client', function () { - const baseUrl = 'https://example.com/'; - assert.equal(tumblr.createClient({ consumer_key: 'abc123', baseUrl }).baseUrl, baseUrl); - - const baseUrlNoSlash = 'https://example.com'; - assert.equal( - tumblr.createClient({ consumer_key: 'abc123', baseUrl: baseUrlNoSlash }).baseUrl, - baseUrl, - ); - }); + /** @type {const} */ ([ + ['createClient', (options) => tumblr.createClient(options)], + ['constructor', (options) => new tumblr.Client(options)], + ]).forEach(([name, factory]) => { + describe(name, () => { + it('createClient produces a Client instance', () => { + const client = factory(); + assert.isTrue(client instanceof tumblr.Client); + }); - it('throws on baseUrl with path', function () { - assert.throws( - () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/v2' }), - 'baseUrl option must not include a pathname.', - ); - }); + it('handles no credentials', function () { + const client = factory(); + assert.deepEqual(client.credentials, { auth: 'none' }); + }); - it('throws on baseUrl with search', function () { - assert.throws( - () => - tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/?params' }), - 'baseUrl option must not include search params (query).', - ); - }); + it('handles apiKey credentials', function () { + const client = factory({ consumer_key: 'abc123' }); + assert.deepEqual(client.credentials, { auth: 'apiKey', apiKey: 'abc123' }); + }); - it('throws on baseUrl with username', function () { - assert.throws( - () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://user@example.com/' }), - 'baseUrl option must not include username.', - ); - }); + it('passes credentials to the client', function () { + const client = factory(DUMMY_CREDENTIALS); + assert.deepEqual(client.credentials, { auth: 'oauth1', ...DUMMY_CREDENTIALS }); + }); - it('throws on baseUrl with password', function () { - assert.throws( - () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://:pw@example.com/' }), - 'baseUrl option must not include password.', - ); - }); + it('passes baseUrl to the client', function () { + const baseUrl = 'https://example.com/'; + assert.equal(factory({ baseUrl }).baseUrl, baseUrl); - it('throws on baseUrl with hash', function () { - assert.throws( - () => tumblr.createClient({ consumer_key: 'abc123', baseUrl: 'https://example.com/#hash' }), - 'baseUrl option must not include hash.', - ); - }); - }); + const baseUrlNoSlash = 'https://example.com'; + assert.equal(factory({ consumer_key: 'abc123', baseUrl: baseUrlNoSlash }).baseUrl, baseUrl); + }); - describe('Client', function () { - const tumblr = require('../lib/tumblr.js'); - const TumblrClient = tumblr.Client; + it('throws on baseUrl with path', function () { + assert.throws( + () => factory({ consumer_key: 'abc123', baseUrl: 'https://example.com/v2' }), + 'baseUrl option must not include a pathname.', + ); + }); - describe('constructor', function () { - it('creates a TumblrClient instance', function () { - const client = new TumblrClient(DUMMY_CREDENTIALS); - assert.isTrue(client instanceof TumblrClient); + it('throws on baseUrl with search', function () { + assert.throws( + () => + factory({ + consumer_key: 'abc123', + baseUrl: 'https://example.com/?params', + }), + 'baseUrl option must not include search params (query).', + ); }); - it('uses the supplied credentials', function () { - const credentials = DUMMY_CREDENTIALS; + it('throws on baseUrl with username', function () { + assert.throws( + () => factory({ consumer_key: 'abc123', baseUrl: 'https://user@example.com/' }), + 'baseUrl option must not include username.', + ); + }); - const client = new TumblrClient(credentials); - assert.equal(client.credentials.consumer_key, credentials.consumer_key); - assert.equal(client.credentials.consumer_secret, credentials.consumer_secret); - assert.equal(client.credentials.token, credentials.token); - assert.equal(client.credentials.token_secret, credentials.token_secret); + it('throws on baseUrl with password', function () { + assert.throws( + () => factory({ consumer_key: 'abc123', baseUrl: 'https://:pw@example.com/' }), + 'baseUrl option must not include password.', + ); }); - it('uses the supplied baseUrl', function () { - const client = tumblr.createClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL }); - assert.equal(client.baseUrl, DUMMY_API_URL.replace(/\/?$/, '/')); + it('throws on baseUrl with hash', function () { + assert.throws( + () => factory({ consumer_key: 'abc123', baseUrl: 'https://example.com/#hash' }), + 'baseUrl option must not include hash.', + ); }); describe('default options', function () { it('uses the default Tumblr API base URL', function () { - const client = tumblr.createClient(DUMMY_CREDENTIALS); + const client = factory(); assert.equal(client.baseUrl, 'https://api.tumblr.com/'); }); - - it('does not return Promises', function () { - const client = tumblr.createClient(DUMMY_CREDENTIALS); - assert.equal(client.getRequest, tumblr.Client.prototype.getRequest); - assert.equal(client.postRequest, tumblr.Client.prototype.postRequest); - }); }); }); + }); + + describe('Client', function () { + const tumblr = require('../lib/tumblr.js'); + const TumblrClient = tumblr.Client; describe('#returnPromises', function () { it('modifies getRequest and postRequest', function () { From 579e9084eabdc057674aa2390cae29aa332d4d1e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:25:49 +0200 Subject: [PATCH 03/74] Improve credentials handling --- lib/tumblr.js | 130 +++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index b157db22..8dd77298 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -467,12 +467,11 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p } } - // Use the `api_key` query param if we don't have full credentials - /** @type {TumblrClient['credentials'] | undefined} */ + /** @type {{} | undefined} */ let oauth; - if (!credentials.token) { - url.searchParams.set('api_key', credentials.consumer_key); - } else { + if (credentials.auth === 'apiKey') { + url.searchParams.set('api_key', credentials.apiKey); + } else if (credentials.auth === 'oauth1') { oauth = credentials; } @@ -493,7 +492,7 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p * Create a function to make POST requests to the Tumblr API * * @param {Function} requestPost - function that performs a get request - * @param {Object} credentials - OAuth credentials + * @param {TumblrClient['credentials']} credentials - OAuth credentials * @param {string} baseUrl - base URL for the request * @param {string} apiPath - URL path for the request * @param {Object} requestOptions - additional request options @@ -518,12 +517,11 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, // Clear the search params url.search = ''; - // Use the `api_key` query param if we don't have full credentials - /** @type {TumblrClient['credentials'] | undefined} */ + /** @type {{} | undefined} */ let oauth; - if (!credentials.token) { - url.searchParams.set('api_key', credentials.consumer_key); - } else { + if (credentials.auth === 'apiKey') { + url.searchParams.set('api_key', credentials.apiKey); + } else if (credentials.auth === 'oauth1') { oauth = credentials; } @@ -622,19 +620,26 @@ function wrapCreatePost(type, validate) { /** * @typedef Options - * @property {string} [baseUrl] The API url, defaults to "https://api.tumblr.com" - * @property {string} consumer_key - * @property {string} [consumer_secret] - * @property {string} [token] - * @property {string} [token_secret] - * + * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. + * @property {string} [consumer_secret] OAuth1 credential. Required for OAuth endpoints. + * @property {string} [token] OAuth1 credential. Required for OAuth endpoints. + * @property {string} [token_secret] OAuth1 credential. Required for Oauth endpoints. + * @property {string} [baseUrl] (optional) The API url if different from the default. + * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. */ class TumblrClient { + /** + * @typedef {{readonly auth:'none'}} NoneAuthCredentials + * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials + * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials + * @typedef {NoneAuthCredentials|ApiKeyCredentials|OAuth1Credentials} Credentials + */ + /** * Creates a Tumblr API client using the given options * - * @param {Options} options - client options + * @param {Options} [options] - client options * * @constructor */ @@ -646,7 +651,7 @@ class TumblrClient { this.version = CLIENT_VERSION; try { - const url = new URL(options.baseUrl ?? API_BASE_URL); + const url = new URL(options?.baseUrl ?? API_BASE_URL); if (url.pathname !== '/') { throw 'pathname'; @@ -696,50 +701,53 @@ class TumblrClient { } } - // A consumer_key is required. It can be used as api_key query param by itself - if (!options.consumer_key || typeof options.consumer_key !== 'string') { - throw new TypeError('You must provide a consumer_key.'); - } + /** @type {Credentials} */ + this.credentials = { auth: 'none' }; + + if (options) { + // If we have any of the optional credentials, we should have all of them. + if ( + /** @type {const} */ (['consumer_secret', 'token_secret', 'token']).some((propertyName) => + Object.prototype.hasOwnProperty.call(options, propertyName), + ) + ) { + if (!options.consumer_key || typeof options.consumer_key !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid consumer_key provided.`, + ); + } + if (!options.consumer_secret || typeof options.consumer_secret !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid consumer_secret provided.`, + ); + } + if (!options.token || typeof options.token !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid token provided.`, + ); + } + if (!options.token_secret || typeof options.token_secret !== 'string') { + throw new TypeError( + `Provide consumer_key or all oauth credentials. Invalid token_secret provided.`, + ); + } - // All or none of these must be provided - const OAUTH_CREDENTIAL_OPTIONAL_PROPERTIES = /** @type {const} */ ([ - 'consumer_secret', - 'token_secret', - 'token', - ]); - - if ( - OAUTH_CREDENTIAL_OPTIONAL_PROPERTIES.some((propertyName) => - Object.prototype.hasOwnProperty.call(options, propertyName), - ) - ) { - if (!options.consumer_secret || typeof options.consumer_secret !== 'string') { - throw new TypeError( - `Provide consumer_key or all oauth credentials. Invalid consumer_secret provided.`, - ); - } - if (!options.token || typeof options.token !== 'string') { - throw new TypeError( - `Provide consumer_key or all oauth credentials. Invalid token provided.`, - ); - } - if (!options.token_secret || typeof options.token_secret !== 'string') { - throw new TypeError( - `Provide consumer_key or all oauth credentials. Invalid token_secret provided.`, - ); + this.credentials = { + auth: 'oauth1', + consumer_key: options.consumer_key, + consumer_secret: options.consumer_secret, + token: options.token, + token_secret: options.token_secret, + }; } - /** - * @type {{readonly consumer_key: string; readonly consumer_secret?: string; readonly token?: string; readonly token_secret?: string}} - */ - this.credentials = { - consumer_key: options.consumer_key, - consumer_secret: options.consumer_secret, - token: options.token, - token_secret: options.token_secret, - }; - } else { - this.credentials = { consumer_key: options.consumer_key }; + // consumer_key can be provided alone to use for api_key authentication + else if (options.consumer_key) { + if (typeof options.consumer_key !== 'string') { + throw new TypeError('You must provide a consumer_key.'); + } + this.credentials = { auth: 'apiKey', apiKey: options.consumer_key }; + } } this.requestOptions = { @@ -1098,7 +1106,7 @@ module.exports = { /** * Creates a Tumblr Client * - * @param {Options} options - client options + * @param {Options} [options] - client options * * @return {TumblrClient} {@link TumblrClient} instance * From d798b3be2649d7bc00aed62b892fa6009b41568a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:31:18 +0200 Subject: [PATCH 04/74] fixup! Work on 4.0.0 refactor --- lib/tumblr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tumblr.js b/lib/tumblr.js index 8dd77298..b3cb39b0 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -378,6 +378,7 @@ function createFunction(name, args, fn) { * @return {Function} function that returns a Promise that resolves with the response body or * rejects with the error message * + * @private */ function promisifyRequest(requestMethod) { /** @this {TumblrClient} */ From d17adf2bdc16bffd01fce51d358061190e0c8d12 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:35:43 +0200 Subject: [PATCH 05/74] Remove lodash/get --- lib/tumblr.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index b3cb39b0..5b80bfc3 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -10,7 +10,6 @@ */ const request = require('request'); -const get = require('lodash/get'); const set = require('lodash/set'); const keys = require('lodash/keys'); const intersection = require('lodash/intersection'); @@ -903,7 +902,7 @@ class TumblrClient { this.createVideoPost = wrapCreatePost('video', ['data', 'data64', 'embed']); // Enable Promise mode - if (get(options, 'returnPromises', false)) { + if (options?.returnPromises)) { this.returnPromises(); } } From 9132ac77b4dfd3ff0fb38294af96119581de3102 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:40:08 +0200 Subject: [PATCH 06/74] fixup! Remove lodash/get --- lib/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 5b80bfc3..57fbd598 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -902,7 +902,7 @@ class TumblrClient { this.createVideoPost = wrapCreatePost('video', ['data', 'data64', 'embed']); // Enable Promise mode - if (options?.returnPromises)) { + if (options?.returnPromises) { this.returnPromises(); } } From 0f7ed3df7b04c70490d923dc78e92e9b05b5f9a7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:40:17 +0200 Subject: [PATCH 07/74] Remove lodash/set --- lib/tumblr.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 57fbd598..c1429562 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -10,7 +10,6 @@ */ const request = require('request'); -const set = require('lodash/set'); const keys = require('lodash/keys'); const intersection = require('lodash/intersection'); const extend = require('lodash/extend'); @@ -1077,7 +1076,7 @@ class TumblrClient { return this[requestMethod](buildApiPath(apiPathArgs), params, callback); }.bind(this); - set(this, methodName, createFunction(methodName, namedParams, methodBody)); + this[methodName] = createFunction(methodName, namedParams, methodBody); } } From 274cb56555fdefe6142b5db519d41599e954666b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 1 Aug 2023 16:42:08 +0200 Subject: [PATCH 08/74] Remove request --- package-lock.json | 411 ++-------------------------------------------- package.json | 4 +- 2 files changed, 17 insertions(+), 398 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7851d008..902a8a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,7 @@ "dependencies": { "@types/lodash": "^4.0.0", "@types/node": ">=16", - "@types/request": "^2.0.0", - "lodash": "^4.17.11", - "request": "^2.88.2" + "lodash": "^4.17.11" }, "devDependencies": { "@tsconfig/node16": "^16.1.0", @@ -668,11 +666,6 @@ "integrity": "sha512-7JHHCbyCsGUxLd0pDbp24yz3zjxw2t673W5oAP6HCEdr/UUhaRhYd3SSnUsGCk+VnPVJVA4mXROzbhI+nyIk+w==", "dev": true }, - "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "node_modules/@types/chai": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", @@ -717,35 +710,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.39.tgz", "integrity": "sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ==" }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -784,6 +748,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -862,22 +827,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -887,38 +836,12 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1047,11 +970,6 @@ } ] }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -1179,17 +1097,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1222,17 +1129,6 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1298,14 +1194,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1336,15 +1224,6 @@ "node": ">=6.0.0" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.468", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.468.tgz", @@ -1653,28 +1532,17 @@ "node": ">=0.10.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -1789,27 +1657,6 @@ "node": ">=8.0.0" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -1886,14 +1733,6 @@ "node": ">=8.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -1947,27 +1786,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2017,20 +1835,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2174,7 +1978,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -2203,11 +2008,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -2359,11 +2159,6 @@ "xmlcreate": "^2.0.4" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, "node_modules/jsdoc": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", @@ -2426,15 +2221,11 @@ "node": ">=4" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -2445,7 +2236,8 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -2459,20 +2251,6 @@ "node": ">=6" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", @@ -2699,25 +2477,6 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3092,14 +2851,6 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3240,11 +2991,6 @@ "node": "*" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3372,27 +3118,15 @@ "node": ">= 8" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3446,37 +3180,6 @@ "node": ">=4" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3561,12 +3264,8 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/semver": { "version": "6.3.1", @@ -3651,30 +3350,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3775,34 +3450,6 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3904,37 +3551,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 43b84dd2..7836c758 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,7 @@ "dependencies": { "@types/lodash": "^4.0.0", "@types/node": ">=16", - "@types/request": "^2.0.0", - "lodash": "^4.17.11", - "request": "^2.88.2" + "lodash": "^4.17.11" }, "devDependencies": { "@tsconfig/node16": "^16.1.0", From 3b74606fb1548940be524ebef865728abd504446 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 17:25:08 +0200 Subject: [PATCH 09/74] Add oauth package --- package-lock.json | 17 ++++++++++++++++- package.json | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 902a8a23..b0bdd4f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "@types/lodash": "^4.0.0", "@types/node": ">=16", - "lodash": "^4.17.11" + "@types/oauth": "^0.9.1", + "lodash": "^4.17.11", + "oauth": "^0.10.0" }, "devDependencies": { "@tsconfig/node16": "^16.1.0", @@ -710,6 +712,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.39.tgz", "integrity": "sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ==" }, + "node_modules/@types/oauth": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2851,6 +2861,11 @@ "node": ">=8" } }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 7836c758..80b23271 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "dependencies": { "@types/lodash": "^4.0.0", "@types/node": ">=16", - "lodash": "^4.17.11" + "@types/oauth": "^0.9.1", + "lodash": "^4.17.11", + "oauth": "^0.10.0" }, "devDependencies": { "@tsconfig/node16": "^16.1.0", From ec9ef22f715e41613dddc10f898a9a734b68d33e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 17:39:40 +0200 Subject: [PATCH 10/74] Get requests almost working --- lib/tumblr.js | 257 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 183 insertions(+), 74 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index c1429562..19e32699 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -9,7 +9,10 @@ * @namespace tumblr */ -const request = require('request'); +const http = require('node:http'); +const https = require('node:https'); +const { URL } = require('node:url'); +const oauth = require('oauth'); const keys = require('lodash/keys'); const intersection = require('lodash/intersection'); const extend = require('lodash/extend'); @@ -414,37 +417,59 @@ function promisifyRequest(requestMethod) { * * @private */ -function requestCallback(callback) { - if (!callback) { - return undefined; - } +function makeCallback(callback) { + return ( + /** @type {(res: http.IncomingMessage) => void} */ + function (res) { + // If we don't have a callback, consume response to free memory + if (!callback) { + res.resume(); + return; + } - return function (err, response, body) { - if (err) { - return callback(err, null, response); - } + res.on('error', (err) => { + callback(err, null, res); + }); - if (response.statusCode >= 400) { - const errString = body.meta ? body.meta.msg : body.error; - return callback( - new Error('API error: ' + response.statusCode + ' ' + errString), - null, - response, - ); - } + let rawData = ''; + res.on('data', (chunk) => { + rawData += chunk; + console.log({ chunk, rawData }); + }); - if (body && body.response) { - return callback(null, body.response, response); - } else { - return callback(new Error('API error (malformed API response): ' + body), null, response); + res.on('end', () => { + try { + const parsedData = JSON.parse(rawData); + + if (/** @type {number} */ (res.statusCode) >= 400) { + const errString = parsedData.meta ? parsedData.meta.msg : parsedData.error; + return callback(new Error('API error: ' + res.statusCode + ' ' + errString), null, res); + } + + if (parsedData && parsedData.response) { + return callback(null, parsedData.response, response); + } else { + return callback( + new Error('API error (malformed API response): ' + parsedData), + null, + res, + ); + } + + callback(null, parsedData, res); + } catch (err) { + callback(err, null, res); + } + }); + return; } - }; + ); } /** * Make a get request * - * @param {Function} requestGet - function that performs a get request + * @param {null|oauth.OAuth} oauth - function that performs a get request * @param {TumblrClient['credentials']} credentials - OAuth credentials * @param {string} baseUrl - base URL for the request * @param {string} apiPath - URL path for the request @@ -452,11 +477,11 @@ function requestCallback(callback) { * @param {Object} params - query parameters * @param {TumblrClientCallback} callback - request callback * - * @return {Request} Request object + * @return {http.ClientRequest} Request object * * @private */ -function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, params, callback) { +function getRequest(oauth, credentials, baseUrl, apiPath, requestOptions, params, callback) { params = params || {}; const url = new URL(apiPath, baseUrl); @@ -466,31 +491,99 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p } } - /** @type {{} | undefined} */ - let oauth; if (credentials.auth === 'apiKey') { url.searchParams.set('api_key', credentials.apiKey); - } else if (credentials.auth === 'oauth1') { - oauth = credentials; } - return requestGet( - extend( - { - url: url.toString(), - oauth, - json: true, - }, - requestOptions, - ), - requestCallback(callback), - ); + /** @type {http.ClientRequest} */ + const request = makeRequest(url, 'GET', callback); + + if (oauth && credentials.auth === 'oauth1') { + const authHeader = oauth.authHeader( + url.toString(), + credentials.token, + credentials.token_secret, + ); + request.setHeader('Authorization', authHeader); + } + return request; +} + +/** + * @param {URL} url + * @param {'GET'|'POST'} method request method + * @param {TumblrClientCallback} callback + */ +function makeRequest(url, method, callback) { + const httpModel = url.protocol === 'http' ? http : https; + const request = httpModel.request(url, { method }); + request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); + + var data = ''; + var callbackCalled = false; + + request.on('response', function (response) { + response.setEncoding('utf8'); + response.on('data', function (chunk) { + data += chunk; + }); + response.on('end', function () { + if (callbackCalled) { + return; + } + callbackCalled = true; + + /** @type {{} | undefined} */ + let parsedData; + try { + parsedData = JSON.parse(data); + } catch (err) { + callback(new Error(`API error (malformed API response): ${data}`), null, response); + return; + } + + const statusCode = /** @type {number} */ (response.statusCode); + if (statusCode < 200 || statusCode > 399) { + // @ts-expect-error unknown shape of parsedData + const errString = parsedData?.meta?.msg ?? parsedData?.error ?? 'unknown'; + return callback( + new Error(`API error: ${response.statusCode} ${errString}`), + null, + response, + ); + } + + // @ts-expect-error Unknown shape of parsedData + if (parsedData && parsedData.response) { + // @ts-expect-error Unknown shape of parsedData + return callback(null, parsedData.response, response); + } else { + return callback( + new Error('API error (malformed API response): ' + parsedData), + null, + response, + ); + } + }); + }); + + request.on('error', function (err) { + if (callbackCalled) { + return; + } + callbackCalled = true; + callback(err, null); + }); + + request.end(); + + return request; } /** * Create a function to make POST requests to the Tumblr API * - * @param {Function} requestPost - function that performs a get request + * @param {null|oauth.OAuth} oauth - function that performs a get request * @param {TumblrClient['credentials']} credentials - OAuth credentials * @param {string} baseUrl - base URL for the request * @param {string} apiPath - URL path for the request @@ -502,7 +595,7 @@ function getRequest(requestGet, credentials, baseUrl, apiPath, requestOptions, p * * @private */ -function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, params, callback) { +function postRequest(oauth, credentials, baseUrl, apiPath, requestOptions, params, callback) { params = params || {}; const url = new URL(apiPath, baseUrl); @@ -516,24 +609,24 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, // Clear the search params url.search = ''; - /** @type {{} | undefined} */ - let oauth; - if (credentials.auth === 'apiKey') { - url.searchParams.set('api_key', credentials.apiKey); - } else if (credentials.auth === 'oauth1') { - oauth = credentials; - } + /** @type {http.ClientRequest} */ + let request; + if (credentials.auth === 'oauth1' && oauth) { + request = oauth.post( + url.toString(), + credentials.token, + credentials.token_secret, + params, + undefined, + makeCallback(callback), + ); + } else { + if (credentials.auth === 'apiKey') { + url.searchParams.set('api_key', credentials.apiKey); + } - // Sign without multipart data - const currentRequest = requestPost( - extend( - { - url: url.toString(), - oauth, - }, - requestOptions, - ), - function (err, response, body) { + const httpModel = url.protocol === 'http' ? http : https; + request = httpModel.request(url, { method: 'POST' }, function (err, response, body) { try { body = JSON.parse(body); } catch (e) { @@ -541,17 +634,19 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, error: 'Malformed Response: ' + body, }; } - requestCallback(callback)(err, response, body); - }, - ); + makeCallback(callback)(err, response, body); + }); + } + + request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); // Sign it with the non-data parameters const dataKeys = ['data']; - currentRequest.form(omit(params, dataKeys)); + request.form(omit(params, dataKeys)); // Clear the side effects from form(param) - delete currentRequest.headers['content-type']; - delete currentRequest.body; + delete request.headers['content-type']; + delete request.body; // if 'data' is an array, rename it with indices if ('data' in params && Array.isArray(params.data)) { @@ -562,15 +657,15 @@ function postRequest(requestPost, credentials, baseUrl, apiPath, requestOptions, } // And then add the full body - const form = currentRequest.form(); + const form = request.form(); for (const key in params) { form.append(key, params[key]); } // Add the form header back - extend(currentRequest.headers, form.getHeaders()); + extend(request.headers, form.getHeaders()); - return currentRequest; + return request; } /** @@ -749,12 +844,26 @@ class TumblrClient { } } - this.requestOptions = { + this.requestOptions = /** @type {const} */ ({ followRedirect: false, headers: { - 'User-Agent': 'tumblr.js/' + CLIENT_VERSION, + 'User-Agent': 'tumblr.js/' + this.version, }, - }; + }); + + /** @type {oauth.OAuth | null} */ + this.oauthClient = + this.credentials.auth === 'oauth1' + ? new oauth.OAuth( + '', + '', + this.credentials.consumer_key, + this.credentials.consumer_secret, + '1.0', + null, + 'HMAC-SHA1', + ) + : null; this.addGetMethods(API_METHODS.GET); this.addPostMethods(API_METHODS.POST); @@ -922,7 +1031,7 @@ class TumblrClient { } return getRequest( - request.get, + this.oauthClient, this.credentials, this.baseUrl, apiPath, @@ -947,7 +1056,7 @@ class TumblrClient { } return postRequest( - request.post, + this.oauthClient, this.credentials, this.baseUrl, apiPath, @@ -1086,7 +1195,7 @@ class TumblrClient { * @callback TumblrClientCallback * @param {?Error} err - error message * @param {?Object} resp - response body - * @param {?string} [response] - raw response + * @param {?http.IncomingMessage} [response] - raw response * @returns {void} */ From f6ca4a8a2de2d0a05315252f49ab1d6ca19ac8f4 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 18:08:16 +0200 Subject: [PATCH 11/74] Fix up all the request stuff --- lib/tumblr.js | 431 ++++++++++++++++---------------------------------- 1 file changed, 134 insertions(+), 297 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 19e32699..55c2a049 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -408,266 +408,6 @@ function promisifyRequest(requestMethod) { }; } -/** - * Wraps a function for use as a request callback - * - * @param {TumblrClientCallback} callback - function to wrap - * - * @return {TumblrClientCallback} request callback - * - * @private - */ -function makeCallback(callback) { - return ( - /** @type {(res: http.IncomingMessage) => void} */ - function (res) { - // If we don't have a callback, consume response to free memory - if (!callback) { - res.resume(); - return; - } - - res.on('error', (err) => { - callback(err, null, res); - }); - - let rawData = ''; - res.on('data', (chunk) => { - rawData += chunk; - console.log({ chunk, rawData }); - }); - - res.on('end', () => { - try { - const parsedData = JSON.parse(rawData); - - if (/** @type {number} */ (res.statusCode) >= 400) { - const errString = parsedData.meta ? parsedData.meta.msg : parsedData.error; - return callback(new Error('API error: ' + res.statusCode + ' ' + errString), null, res); - } - - if (parsedData && parsedData.response) { - return callback(null, parsedData.response, response); - } else { - return callback( - new Error('API error (malformed API response): ' + parsedData), - null, - res, - ); - } - - callback(null, parsedData, res); - } catch (err) { - callback(err, null, res); - } - }); - return; - } - ); -} - -/** - * Make a get request - * - * @param {null|oauth.OAuth} oauth - function that performs a get request - * @param {TumblrClient['credentials']} credentials - OAuth credentials - * @param {string} baseUrl - base URL for the request - * @param {string} apiPath - URL path for the request - * @param {Object} requestOptions - additional request options - * @param {Object} params - query parameters - * @param {TumblrClientCallback} callback - request callback - * - * @return {http.ClientRequest} Request object - * - * @private - */ -function getRequest(oauth, credentials, baseUrl, apiPath, requestOptions, params, callback) { - params = params || {}; - - const url = new URL(apiPath, baseUrl); - if (params) { - for (const [key, value] of Object.entries(params)) { - url.searchParams.set(key, value); - } - } - - if (credentials.auth === 'apiKey') { - url.searchParams.set('api_key', credentials.apiKey); - } - - /** @type {http.ClientRequest} */ - const request = makeRequest(url, 'GET', callback); - - if (oauth && credentials.auth === 'oauth1') { - const authHeader = oauth.authHeader( - url.toString(), - credentials.token, - credentials.token_secret, - ); - request.setHeader('Authorization', authHeader); - } - return request; -} - -/** - * @param {URL} url - * @param {'GET'|'POST'} method request method - * @param {TumblrClientCallback} callback - */ -function makeRequest(url, method, callback) { - const httpModel = url.protocol === 'http' ? http : https; - const request = httpModel.request(url, { method }); - request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); - - var data = ''; - var callbackCalled = false; - - request.on('response', function (response) { - response.setEncoding('utf8'); - response.on('data', function (chunk) { - data += chunk; - }); - response.on('end', function () { - if (callbackCalled) { - return; - } - callbackCalled = true; - - /** @type {{} | undefined} */ - let parsedData; - try { - parsedData = JSON.parse(data); - } catch (err) { - callback(new Error(`API error (malformed API response): ${data}`), null, response); - return; - } - - const statusCode = /** @type {number} */ (response.statusCode); - if (statusCode < 200 || statusCode > 399) { - // @ts-expect-error unknown shape of parsedData - const errString = parsedData?.meta?.msg ?? parsedData?.error ?? 'unknown'; - return callback( - new Error(`API error: ${response.statusCode} ${errString}`), - null, - response, - ); - } - - // @ts-expect-error Unknown shape of parsedData - if (parsedData && parsedData.response) { - // @ts-expect-error Unknown shape of parsedData - return callback(null, parsedData.response, response); - } else { - return callback( - new Error('API error (malformed API response): ' + parsedData), - null, - response, - ); - } - }); - }); - - request.on('error', function (err) { - if (callbackCalled) { - return; - } - callbackCalled = true; - callback(err, null); - }); - - request.end(); - - return request; -} - -/** - * Create a function to make POST requests to the Tumblr API - * - * @param {null|oauth.OAuth} oauth - function that performs a get request - * @param {TumblrClient['credentials']} credentials - OAuth credentials - * @param {string} baseUrl - base URL for the request - * @param {string} apiPath - URL path for the request - * @param {Object} requestOptions - additional request options - * @param {Object} params - form data - * @param {TumblrClientCallback} callback - request callback - * - * @return {Request} Request object - * - * @private - */ -function postRequest(oauth, credentials, baseUrl, apiPath, requestOptions, params, callback) { - params = params || {}; - - const url = new URL(apiPath, baseUrl); - - // Move URL search params to send them in the request body - for (const [key, value] of url.searchParams.entries()) { - if (!Object.prototype.hasOwnProperty.call(params, key)) { - params[key] = value; - } - } - // Clear the search params - url.search = ''; - - /** @type {http.ClientRequest} */ - let request; - if (credentials.auth === 'oauth1' && oauth) { - request = oauth.post( - url.toString(), - credentials.token, - credentials.token_secret, - params, - undefined, - makeCallback(callback), - ); - } else { - if (credentials.auth === 'apiKey') { - url.searchParams.set('api_key', credentials.apiKey); - } - - const httpModel = url.protocol === 'http' ? http : https; - request = httpModel.request(url, { method: 'POST' }, function (err, response, body) { - try { - body = JSON.parse(body); - } catch (e) { - body = { - error: 'Malformed Response: ' + body, - }; - } - makeCallback(callback)(err, response, body); - }); - } - - request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); - - // Sign it with the non-data parameters - const dataKeys = ['data']; - request.form(omit(params, dataKeys)); - - // Clear the side effects from form(param) - delete request.headers['content-type']; - delete request.body; - - // if 'data' is an array, rename it with indices - if ('data' in params && Array.isArray(params.data)) { - for (let i = 0; i < params.data.length; ++i) { - params['data[' + i + ']'] = params.data[i]; - } - delete params.data; - } - - // And then add the full body - const form = request.form(); - for (const key in params) { - form.append(key, params[key]); - } - - // Add the form header back - extend(request.headers, form.getHeaders()); - - return request; -} - /** * Wraps createPost to specify `type` and validate the parameters * @@ -844,13 +584,6 @@ class TumblrClient { } } - this.requestOptions = /** @type {const} */ ({ - followRedirect: false, - headers: { - 'User-Agent': 'tumblr.js/' + this.version, - }, - }); - /** @type {oauth.OAuth | null} */ this.oauthClient = this.credentials.auth === 'oauth1' @@ -1019,52 +752,156 @@ class TumblrClient { * Performs a GET request * * @param {string} apiPath - URL path for the request - * @param {Object} params - query parameters - * @param {TumblrClientCallback} callback - request callback + * @param {Record} [params] - query parameters + * @param {TumblrClientCallback} [callback] - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ - getRequest(apiPath, params, callback) { - if (typeof params === 'function') { - callback = params; - params = {}; + getRequest(apiPath, params = {}, callback) { + const url = new URL(apiPath, this.baseUrl); + if (params) { + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } } - return getRequest( - this.oauthClient, - this.credentials, - this.baseUrl, - apiPath, - this.requestOptions, - params, - callback, - ); + return this.makeRequest(url, 'GET', null, callback); } + + /** + * @typedef RequestData + * @property {'multipart/form-data'|'application/json'} encoding + * @property {Record} data + */ + + /** + * @param {URL} url + * @param {'GET'|'POST'} method request method + * @param {null|RequestData} data + * @param {TumblrClientCallback} [callback] + * + * @returns {http.ClientRequest} + * + * @private + */ + makeRequest(url, method, data, callback) { + const httpModel = url.protocol === 'http' ? http : https; + + if (this.credentials.auth === 'apiKey') { + url.searchParams.set('api_key', this.credentials.apiKey); + } + + const request = httpModel.request(url, { method }); + request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); + + if (this.oauthClient && this.credentials.auth === 'oauth1') { + const authHeader = this.oauthClient.authHeader( + url.toString(), + this.credentials.token, + this.credentials.token_secret, + ); + request.setHeader('Authorization', authHeader); + } + + if (data) { + request.setHeader('Content-Type', data.encoding); + request.write(JSON.stringify(data.data)); + } + + var responseData = ''; + var callbackCalled = false; + + request.on('response', function (response) { + if (!callback) { + response.resume(); + return; + } + + response.setEncoding('utf8'); + response.on('data', function (chunk) { + responseData += chunk; + }); + response.on('end', function () { + if (callbackCalled) { + return; + } + callbackCalled = true; + + /** @type {{} | undefined} */ + let parsedData; + try { + parsedData = JSON.parse(responseData); + } catch (err) { + callback( + new Error(`API error (malformed API response): ${responseData}`), + null, + response, + ); + return; + } + + const statusCode = /** @type {number} */ (response.statusCode); + if (statusCode < 200 || statusCode > 399) { + // @ts-expect-error unknown shape of parsedData + const errString = parsedData?.meta?.msg ?? parsedData?.error ?? 'unknown'; + return callback( + new Error(`API error: ${response.statusCode} ${errString}`), + null, + response, + ); + } + + // @ts-expect-error Unknown shape of parsedData + if (parsedData && parsedData.response) { + // @ts-expect-error Unknown shape of parsedData + return callback(null, parsedData.response, response); + } else { + return callback( + new Error('API error (malformed API response): ' + parsedData), + null, + response, + ); + } + }); + }); + + request.on('error', function (err) { + if (callbackCalled) { + return; + } + callbackCalled = true; + callback?.(err, null); + }); + + request.end(); + + return request; + } + /** * Performs a POST request * * @param {string} apiPath - URL path for the request - * @param {Object} params - form parameters - * @param {TumblrClientCallback} callback - request callback + * @param {Record} [params] - form parameters + * @param {TumblrClientCallback} [callback] - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ - postRequest(apiPath, params, callback) { - if (typeof params === 'function') { - callback = params; - params = {}; + postRequest(apiPath, params = {}, callback) { + const url = new URL(apiPath, this.baseUrl); + + // Move URL search params to send them in the request body + for (const [key, value] of url.searchParams.entries()) { + if (!Object.prototype.hasOwnProperty.call(params, key)) { + params[key] = value; + } } + // Clear the search params + url.search = ''; - return postRequest( - this.oauthClient, - this.credentials, - this.baseUrl, - apiPath, - this.requestOptions, - params, - callback, - ); + return this.makeRequest(url, 'POST', { encoding: 'application/json', data: params }, callback); } + /** * Sets the client to return Promises instead of Request objects by patching the `getRequest` and * `postRequest` methods on the client From 3ca54297c9dfc2ae14683b0c02420e1224c29d34 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 18:14:40 +0200 Subject: [PATCH 12/74] Add json accept header --- lib/tumblr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tumblr.js b/lib/tumblr.js index 55c2a049..5b2f5131 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -793,6 +793,7 @@ class TumblrClient { const request = httpModel.request(url, { method }); request.setHeader('User-Agent', 'tumblr.js/' + CLIENT_VERSION); + request.setHeader('Accept', 'application/json'); if (this.oauthClient && this.credentials.auth === 'oauth1') { const authHeader = this.oauthClient.authHeader( From 42c79b7a8b37ba3bb8843c37fc73fe80eb74ac58 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 18:34:49 +0200 Subject: [PATCH 13/74] Working json encoding --- lib/tumblr.js | 20 +++++++--- test/tumblr.test.js | 89 ++++++++++++++++++++++++++++----------------- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 5b2f5131..fa070f0c 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -23,7 +23,6 @@ const isString = require('lodash/isString'); const isFunction = require('lodash/isFunction'); const isArray = require('lodash/isArray'); const isPlainObject = require('lodash/isPlainObject'); -const omit = require('lodash/omit'); const CLIENT_VERSION = '4.0.0-alpha.0'; const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash @@ -805,8 +804,10 @@ class TumblrClient { } if (data) { + const content = JSON.stringify(data.data); request.setHeader('Content-Type', data.encoding); - request.write(JSON.stringify(data.data)); + request.setHeader('Content-Length', content.length); + request.write(content); } var responseData = ''; @@ -891,16 +892,25 @@ class TumblrClient { postRequest(apiPath, params = {}, callback) { const url = new URL(apiPath, this.baseUrl); + const requestData = new Map(Object.entries(params)); + // Move URL search params to send them in the request body for (const [key, value] of url.searchParams.entries()) { - if (!Object.prototype.hasOwnProperty.call(params, key)) { - params[key] = value; + if (!requestData.has(key)) { + requestData.set(key, value); } } // Clear the search params url.search = ''; - return this.makeRequest(url, 'POST', { encoding: 'application/json', data: params }, callback); + return this.makeRequest( + url, + 'POST', + requestData.size + ? { encoding: 'application/json', data: Object.fromEntries(requestData.entries()) } + : null, + callback, + ); } /** diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 72a09d00..2167a38e 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -1,12 +1,7 @@ const tumblr = require('../lib/tumblr.js'); - const fs = require('fs'); const path = require('path'); - -// @ts-expect-error -const Request = require('request').Request; const JSON5 = require('json5'); - const assert = require('chai').assert; const nock = require('nock'); @@ -200,8 +195,8 @@ describe('tumblr.js', function () { client.returnPromises(); const scope = nock(client.baseUrl, { reqheaders: { - 'user-agent': `tumblr.js/${client.version}`, accept: 'application/json', + 'user-agent': `tumblr.js/${client.version}`, authorization: (value) => { return [ value.startsWith('OAuth '), @@ -219,7 +214,6 @@ describe('tumblr.js', function () { .get('/') .reply(200, { meta: {}, response: {} }); - // @ts-expect-error Promise request with no params, this is OK. assert.isOk(await client.getRequest('/')); scope.done(); }); @@ -234,38 +228,67 @@ describe('tumblr.js', function () { .query({ api_key: 'abc123' }) .reply(200, { meta: {}, response: {} }); - // @ts-expect-error Promise request with no params, this is OK. assert.isOk(await client.getRequest('/')); scope.done(); }); - it('post request expected headers', async () => { - client.returnPromises(); - const scope = nock(client.baseUrl, { - reqheaders: { - 'user-agent': `tumblr.js/${client.version}`, - 'content-type': /^multipart\/form-data; ?boundary=-*\d+/, - 'content-length': /^\d+/, - authorization: (value) => { - return [ - value.startsWith('OAuth '), - value.includes('oauth_signature_method="HMAC-SHA1"'), - value.includes('oauth_version="1.0"'), - value.includes(`oauth_consumer_key="${DUMMY_CREDENTIALS.consumer_key}"`), - value.includes(`oauth_token="${DUMMY_CREDENTIALS.token}"`), - /oauth_nonce="[^"]+"/.test(value), - /oauth_timestamp="[^"]+"/.test(value), - /oauth_signature="[^"]+"/.test(value), - ].every((passes) => passes); + describe('post request expected headers', () => { + it('with body', async () => { + client.returnPromises(); + const scope = nock(client.baseUrl, { + reqheaders: { + accept: 'application/json', + 'user-agent': `tumblr.js/${client.version}`, + 'content-type': 'application/json', + 'content-length': '13', + authorization: (value) => { + return [ + value.startsWith('OAuth '), + value.includes('oauth_signature_method="HMAC-SHA1"'), + value.includes('oauth_version="1.0"'), + value.includes(`oauth_consumer_key="${DUMMY_CREDENTIALS.consumer_key}"`), + value.includes(`oauth_token="${DUMMY_CREDENTIALS.token}"`), + /oauth_nonce="[^"]+"/.test(value), + /oauth_timestamp="[^"]+"/.test(value), + /oauth_signature="[^"]+"/.test(value), + ].every((passes) => passes); + }, }, - }, - }) - .post('/') - .reply(200, { meta: {}, response: {} }); + }) + .post('/', '{"foo":"bar"}') + .reply(200, { meta: {}, response: {} }); - // @ts-expect-error Promise request with no params, this is OK. - assert.isOk(await client.postRequest('/')); - scope.done(); + assert.isOk(await client.postRequest('/', { foo: 'bar' })); + scope.done(); + }); + + it('without body', async () => { + client.returnPromises(); + const scope = nock(client.baseUrl, { + badheaders: ['content-length', 'content-type'], + reqheaders: { + accept: 'application/json', + 'user-agent': `tumblr.js/${client.version}`, + authorization: (value) => { + return [ + value.startsWith('OAuth '), + value.includes('oauth_signature_method="HMAC-SHA1"'), + value.includes('oauth_version="1.0"'), + value.includes(`oauth_consumer_key="${DUMMY_CREDENTIALS.consumer_key}"`), + value.includes(`oauth_token="${DUMMY_CREDENTIALS.token}"`), + /oauth_nonce="[^"]+"/.test(value), + /oauth_timestamp="[^"]+"/.test(value), + /oauth_signature="[^"]+"/.test(value), + ].every((passes) => passes); + }, + }, + }) + .post('/') + .reply(200, { meta: {}, response: {} }); + + assert.isOk(await client.postRequest('/')); + scope.done(); + }); }); it('get request sends api_key when all creds are not provided', async () => { From d2d5ce28e9d1ded540b82d72461216ecb98f4ded Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 19:47:00 +0200 Subject: [PATCH 14/74] Fix up tests --- test/tumblr.test.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 2167a38e..60665a38 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -291,7 +291,7 @@ describe('tumblr.js', function () { }); }); - it('get request sends api_key when all creds are not provided', async () => { + it('post request sends api_key when all creds are not provided', async () => { const client = new TumblrClient({ consumer_key: 'abc123' }); client.returnPromises(); const scope = nock(client.baseUrl, { @@ -301,7 +301,6 @@ describe('tumblr.js', function () { .query({ api_key: 'abc123' }) .reply(200, { meta: {}, response: {} }); - // @ts-expect-error Promise request with no params, this is OK. assert.isOk(await client.postRequest('/')); scope.done(); }); @@ -356,7 +355,7 @@ describe('tumblr.js', function () { it('returns a Request'); } else { it('returns a Request', function () { - assert.isTrue(returnValue instanceof Request); + assert.isTrue(returnValue instanceof require('http').ClientRequest); }); } @@ -376,7 +375,6 @@ describe('tumblr.js', function () { requestError = false; requestResponse = false; - // @ts-expect-error This is a bad function signature - optionals in middle @TODO client[clientMethod]( apiPath, /** @param {any} args */ @@ -431,7 +429,6 @@ describe('tumblr.js', function () { requestError = false; requestResponse = false; - // @ts-expect-error It's promises, no callback returnValue = client[clientMethod](apiPath, params); // Invoke the callback when the Promise resolves or rejects returnValue.then( @@ -530,10 +527,6 @@ describe('tumblr.js', function () { requestError = false; requestResponse = false; - if (client.credentials.consumer_key) { - queryParams.api_key = client.credentials.consumer_key; - } - const scope = nock(client.baseUrl)[httpMethod](apiPath); if (params.length) { scope.query(true); From f7f46da74b903ed1504eb0bc42b1608781034100 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 20:44:33 +0200 Subject: [PATCH 15/74] Temporarily disable unit tests --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6765e1f7..9b78d854 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,9 +42,9 @@ jobs: npm run lint npm run format-check - - name: Run tests - run: | - npm run test:coverage + # - name: Run tests + # run: | + # npm run test:coverage - name: Run integration tests env: From ba54137c143596dd280dc9ca679672916c5378c9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 20:45:29 +0200 Subject: [PATCH 16/74] Switch to form data --- lib/tumblr.js | 14 ++++++++---- package-lock.json | 57 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index fa070f0c..0e298f4f 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -9,6 +9,7 @@ * @namespace tumblr */ +const FormData = require('form-data'); const http = require('node:http'); const https = require('node:https'); const { URL } = require('node:url'); @@ -804,10 +805,15 @@ class TumblrClient { } if (data) { - const content = JSON.stringify(data.data); - request.setHeader('Content-Type', data.encoding); - request.setHeader('Content-Length', content.length); - request.write(content); + const form = new FormData(); + for (const [key, value] of Object.entries(data.data)) { + form.append(key, value); + } + + for (const [key, value] of Object.entries(form.getHeaders())) { + request.setHeader(key, value); + } + form.pipe(request); } var responseData = ''; diff --git a/package-lock.json b/package-lock.json index b0bdd4f9..6e38e5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", + "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, @@ -846,6 +847,11 @@ "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1107,6 +1113,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1204,6 +1221,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1667,6 +1692,19 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2487,6 +2525,25 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index 80b23271..ac20c9d0 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", + "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, From 4fe7aa4b8f602d2c3644f13271ccea7694ef6d5d Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 20:53:52 +0200 Subject: [PATCH 17/74] Revert "Switch to form data" This reverts commit ba54137c143596dd280dc9ca679672916c5378c9. --- lib/tumblr.js | 14 ++++-------- package-lock.json | 57 ----------------------------------------------- package.json | 1 - 3 files changed, 4 insertions(+), 68 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 0e298f4f..fa070f0c 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -9,7 +9,6 @@ * @namespace tumblr */ -const FormData = require('form-data'); const http = require('node:http'); const https = require('node:https'); const { URL } = require('node:url'); @@ -805,15 +804,10 @@ class TumblrClient { } if (data) { - const form = new FormData(); - for (const [key, value] of Object.entries(data.data)) { - form.append(key, value); - } - - for (const [key, value] of Object.entries(form.getHeaders())) { - request.setHeader(key, value); - } - form.pipe(request); + const content = JSON.stringify(data.data); + request.setHeader('Content-Type', data.encoding); + request.setHeader('Content-Length', content.length); + request.write(content); } var responseData = ''; diff --git a/package-lock.json b/package-lock.json index 6e38e5a8..b0bdd4f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", - "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, @@ -847,11 +846,6 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1113,17 +1107,6 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1221,14 +1204,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1692,19 +1667,6 @@ "node": ">=8.0.0" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2525,25 +2487,6 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index ac20c9d0..80b23271 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", - "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, From 7bb87434b6282d0bc8269d55940b43a50509de4e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 21:07:20 +0200 Subject: [PATCH 18/74] Fix oauth signature --- lib/tumblr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tumblr.js b/lib/tumblr.js index fa070f0c..05bd6cc0 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -799,6 +799,7 @@ class TumblrClient { url.toString(), this.credentials.token, this.credentials.token_secret, + method, ); request.setHeader('Authorization', authHeader); } From 8550a827eccee1c648a28d0b17011ab078faf257 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 21:08:36 +0200 Subject: [PATCH 19/74] Continue CI job on error --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b78d854..888cf0a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,7 @@ jobs: tests: name: Testing with Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest + continue-on-error: true strategy: matrix: From 67421b124813cce4e7f0158f4c447e1fe079c737 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 21:09:30 +0200 Subject: [PATCH 20/74] Revert "Temporarily disable unit tests" This reverts commit f7f46da74b903ed1504eb0bc42b1608781034100. --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 888cf0a6..62982923 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,9 +43,9 @@ jobs: npm run lint npm run format-check - # - name: Run tests - # run: | - # npm run test:coverage + - name: Run tests + run: | + npm run test:coverage - name: Run integration tests env: From 1ef2e8fc843dbb4a66185f5b3fa4aff9942e25b7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 2 Aug 2023 21:28:19 +0200 Subject: [PATCH 21/74] Revert "Revert "Switch to form data"" This reverts commit 4fe7aa4b8f602d2c3644f13271ccea7694ef6d5d. --- lib/tumblr.js | 14 ++++++++---- package-lock.json | 57 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 05bd6cc0..d09a8ad8 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -9,6 +9,7 @@ * @namespace tumblr */ +const FormData = require('form-data'); const http = require('node:http'); const https = require('node:https'); const { URL } = require('node:url'); @@ -805,10 +806,15 @@ class TumblrClient { } if (data) { - const content = JSON.stringify(data.data); - request.setHeader('Content-Type', data.encoding); - request.setHeader('Content-Length', content.length); - request.write(content); + const form = new FormData(); + for (const [key, value] of Object.entries(data.data)) { + form.append(key, value); + } + + for (const [key, value] of Object.entries(form.getHeaders())) { + request.setHeader(key, value); + } + form.pipe(request); } var responseData = ''; diff --git a/package-lock.json b/package-lock.json index b0bdd4f9..6e38e5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", + "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, @@ -846,6 +847,11 @@ "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1107,6 +1113,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1204,6 +1221,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1667,6 +1692,19 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -2487,6 +2525,25 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index 80b23271..ac20c9d0 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", + "form-data": "^4.0.0", "lodash": "^4.17.11", "oauth": "^0.10.0" }, From 165a3476816979b50173bb73514113b3132fecf5 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 3 Aug 2023 12:20:30 +0200 Subject: [PATCH 22/74] Add photo post (data64) integration test --- integration/write.mjs | 26 +++++++++++++++++++++++++- test/fixtures/image.jpg | Bin 0 -> 25459 bytes 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/image.jpg diff --git a/integration/write.mjs b/integration/write.mjs index c3c9d7f3..d339ae65 100644 --- a/integration/write.mjs +++ b/integration/write.mjs @@ -1,3 +1,5 @@ +import { readFile } from 'node:fs/promises'; +import { URL } from 'node:url'; import { env } from 'node:process'; import { Client } from 'tumblr.js'; import { assert } from 'chai'; @@ -29,8 +31,8 @@ describe('oauth1 write requests', () => { consumer_secret: env.TUMBLR_OAUTH_CONSUMER_SECRET, token: env.TUMBLR_OAUTH_TOKEN, token_secret: env.TUMBLR_OAUTH_TOKEN_SECRET, + returnPromises: true, }); - client.returnPromises(); }); test('creates a post', async () => { @@ -49,4 +51,26 @@ describe('oauth1 write requests', () => { }), ); }); + + describe('image post', () => { + it('creates an image post with data64', async () => { + const imageData = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url), { + encoding: 'base64', + }); + const userResp = await client.userInfo(); + + assert.isOk(userResp); + const blogName = userResp.user.blogs[0].name; + + const res = await client.createPost(blogName, { + type: 'photo', + caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, + link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', + data64: imageData, + tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + }); + console.log({ res }); + assert.isOk(res); + }); + }); }); diff --git a/test/fixtures/image.jpg b/test/fixtures/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0c2ae221f7d93e15257a98130c7a6337e84c6c31 GIT binary patch literal 25459 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 Date: Thu, 3 Aug 2023 23:50:52 +0200 Subject: [PATCH 23/74] Add integration tests for legacy post creation with media --- integration/write.mjs | 67 ++++++++++++++++++++++++++++++---------- lib/tumblr.js | 8 ++++- test/fixtures/audio.mp3 | Bin 0 -> 8282 bytes test/fixtures/image.jpg | Bin 25459 -> 10347 bytes 4 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/audio.mp3 diff --git a/integration/write.mjs b/integration/write.mjs index d339ae65..7a792aca 100644 --- a/integration/write.mjs +++ b/integration/write.mjs @@ -6,9 +6,13 @@ import { assert } from 'chai'; import { test } from 'mocha'; describe('oauth1 write requests', () => { - /** @type {Client} */ + /** @type {import('tumblr.js').Client} */ let client; - before(function () { + + /** @type {string} */ + let blogName; + + before(async function () { if ( !env.TUMBLR_OAUTH_CONSUMER_KEY || !env.TUMBLR_OAUTH_CONSUMER_SECRET || @@ -33,14 +37,12 @@ describe('oauth1 write requests', () => { token_secret: env.TUMBLR_OAUTH_TOKEN_SECRET, returnPromises: true, }); - }); - test('creates a post', async () => { const userResp = await client.userInfo(); + blogName = userResp.user.blogs[0].name; + }); - assert.isOk(userResp); - const blogName = userResp.user.blogs[0].name; - + test('creates a text post', async () => { assert.isOk( await client.createPost(blogName, { type: 'text', @@ -52,25 +54,58 @@ describe('oauth1 write requests', () => { ); }); - describe('image post', () => { - it('creates an image post with data64', async () => { - const imageData = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url), { - encoding: 'base64', + describe('photo post', () => { + it('creates a post via data', async () => { + const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url)); + + const res = await client.createPost(blogName, { + type: 'photo', + caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, + link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', + tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + data: data, + }); + assert.isOk(res); + }); + + it('creates a slideshow post via data[]', async () => { + const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url)); + + const res = await client.createPost(blogName, { + type: 'photo', + caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, + link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', + tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + data: [data, data], }); - const userResp = await client.userInfo(); + assert.isOk(res); + }); - assert.isOk(userResp); - const blogName = userResp.user.blogs[0].name; + it(`creates a post via data64`, async () => { + const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url), { + encoding: 'base64', + }); const res = await client.createPost(blogName, { type: 'photo', caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', - data64: imageData, tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + data64: data, }); - console.log({ res }); assert.isOk(res); }); }); + + it('creates an audio post with data', async () => { + const data = await readFile(new URL('../test/fixtures/audio.mp3', import.meta.url)); + + const res = await client.createPost(blogName, { + type: 'audio', + caption: `Multiple Dog Barks (King Charles Spaniel) || Automated test post ${new Date().toISOString()}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + data: data, + }); + assert.isOk(res); + }); }); diff --git a/lib/tumblr.js b/lib/tumblr.js index d09a8ad8..c30d95c8 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -807,7 +807,13 @@ class TumblrClient { if (data) { const form = new FormData(); - for (const [key, value] of Object.entries(data.data)) { + for (const [key, value] of data.entries()) { + if (key === 'data') { + (Array.isArray(value) ? value : [value]).forEach((arrValue, index) => { + form.append(`${key}[${index}]`, arrValue, { contentType: 'image/jpeg' }); + }); + continue; + } form.append(key, value); } diff --git a/test/fixtures/audio.mp3 b/test/fixtures/audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..bbae894cd8cc811a89f9eefbff067d6d3e62beaf GIT binary patch literal 8282 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 delta 24818 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 From 0c06fe779387193ff91d221827fc3130e30c0566 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 3 Aug 2023 23:55:13 +0200 Subject: [PATCH 24/74] Handle posts --- lib/tumblr.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index c30d95c8..eb9c9c13 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -464,6 +464,8 @@ function wrapCreatePost(type, validate) { class TumblrClient { /** + * @typedef {Map|string>} RequestData + * * @typedef {{readonly auth:'none'}} NoneAuthCredentials * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials @@ -768,12 +770,6 @@ class TumblrClient { return this.makeRequest(url, 'GET', null, callback); } - /** - * @typedef RequestData - * @property {'multipart/form-data'|'application/json'} encoding - * @property {Record} data - */ - /** * @param {URL} url * @param {'GET'|'POST'} method request method @@ -807,10 +803,14 @@ class TumblrClient { if (data) { const form = new FormData(); + + const isLegacyPhotoPost = url.pathname.endsWith('/post') && data.get('type') === 'photo'; + for (const [key, value] of data.entries()) { - if (key === 'data') { + // Legacy photo post creation has a special case to accept `data`. + if (isLegacyPhotoPost && key === 'data') { (Array.isArray(value) ? value : [value]).forEach((arrValue, index) => { - form.append(`${key}[${index}]`, arrValue, { contentType: 'image/jpeg' }); + form.append(`${key}[${index}]`, arrValue); }); continue; } @@ -820,6 +820,7 @@ class TumblrClient { for (const [key, value] of Object.entries(form.getHeaders())) { request.setHeader(key, value); } + form.pipe(request); } @@ -916,14 +917,7 @@ class TumblrClient { // Clear the search params url.search = ''; - return this.makeRequest( - url, - 'POST', - requestData.size - ? { encoding: 'application/json', data: Object.fromEntries(requestData.entries()) } - : null, - callback, - ); + return this.makeRequest(url, 'POST', requestData.size ? requestData : null, callback); } /** @@ -1069,6 +1063,7 @@ module.exports = { * * @memberof tumblr * @see {@link TumblrClient} + * @type {typeof TumblrClient} */ Client: TumblrClient, From 2c3e46087b54a1234d23072b0837216bdb16bfda Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 3 Aug 2023 23:59:01 +0200 Subject: [PATCH 25/74] Drop continue-on-error --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 62982923..6765e1f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,6 @@ jobs: tests: name: Testing with Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest - continue-on-error: true strategy: matrix: From 14949ac1866d6458b08d22330929519b05d32114 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 00:13:26 +0200 Subject: [PATCH 26/74] Deprecate legacy post endpoints --- CHANGELOG.md | 14 ++++++++++++++ lib/tumblr.js | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5667681e..6e9e7f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Breaking** The (optional) `baseUrl` option should be of the form `https://example.com` with no pathname, search, hash, etc. Bad `baseUrl` options will throw. +### Deprecated + +- Deprecated the following legacy post creation methods. Prefer NPF methods (`/posts` endpoint). + - `createPost` + - `createAudioPost` + - `createChatPost` + - `createLinkPost` + - `createPhotoPost` + - `createQuotePost` + - `createTextPost` + - `createVideoPost` + - `editPost` + - `reblogPost` + ### Fixed - `blogIdentifier` parameters will not have `.tumblr.com` automatically appended. Blog UUIDs can now diff --git a/lib/tumblr.js b/lib/tumblr.js index eb9c9c13..f7439948 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -226,6 +226,8 @@ const API_METHODS = { /** * Creates a post on the given blog. * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} * @method createPost * @@ -242,6 +244,8 @@ const API_METHODS = { /** * Edits a given post * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @method editPost * * @param {string} blogIdentifier - blog name or URL @@ -257,6 +261,8 @@ const API_METHODS = { /** * Reblogs a given post * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @method reblogPost * * @param {string} blogIdentifier - blog name or URL @@ -606,6 +612,8 @@ class TumblrClient { /** * Creates a text post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#ptext-posts|API docs} * * @method createTextPost @@ -625,6 +633,8 @@ class TumblrClient { /** * Creates a photo post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#pphoto-posts|API docs} * * @method createPhotoPost @@ -646,6 +656,8 @@ class TumblrClient { /** * Creates a quote post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#pquote-posts|API docs} * * @method createQuotePost @@ -665,6 +677,8 @@ class TumblrClient { /** * Creates a link post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#plink-posts|API docs} * * @method createLinkPost @@ -688,6 +702,8 @@ class TumblrClient { /** * Creates a chat post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#pchat-posts|API docs} * * @method createChatPost @@ -707,6 +723,8 @@ class TumblrClient { /** * Creates an audio post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#paudio-posts|API docs} * * @method createAudioPost @@ -727,6 +745,8 @@ class TumblrClient { /** * Creates a video post on the given blog * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * * @see {@link https://www.tumblr.com/docs/api/v2#pvideo-posts|API docs} * * @method createVideoPost @@ -928,6 +948,7 @@ class TumblrClient { this.getRequest = promisifyRequest(this.getRequest); this.postRequest = promisifyRequest(this.postRequest); } + /** * Adds GET methods to the client * From 4e4dc993dba410acc4393c8f634e881b071b9b70 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 00:13:37 +0200 Subject: [PATCH 27/74] Stop returning request object --- lib/tumblr.js | 4 ---- test/tumblr.test.js | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index f7439948..a18c5b70 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -796,8 +796,6 @@ class TumblrClient { * @param {null|RequestData} data * @param {TumblrClientCallback} [callback] * - * @returns {http.ClientRequest} - * * @private */ makeRequest(url, method, data, callback) { @@ -910,8 +908,6 @@ class TumblrClient { }); request.end(); - - return request; } /** diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 60665a38..5d87d30b 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -349,16 +349,6 @@ describe('tumblr.js', function () { ); }); - if (httpMethod === 'post') { - // Nock seems to cause the POST request to return a Promise, - // making this difficult to properly test. - it('returns a Request'); - } else { - it('returns a Request', function () { - assert.isTrue(returnValue instanceof require('http').ClientRequest); - }); - } - it('invokes the callback', function () { assert.isTrue(callbackInvoked); }); From 348841466d7c17b03e54a91b9f2c4df7e0c44583 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 00:24:23 +0200 Subject: [PATCH 28/74] Test work --- test/tumblr.test.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 5d87d30b..ff4d8e44 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -219,8 +219,7 @@ describe('tumblr.js', function () { }); it('get request sends api_key when all creds are not provided', async () => { - const client = new TumblrClient({ consumer_key: 'abc123' }); - client.returnPromises(); + const client = new TumblrClient({ consumer_key: 'abc123', returnPromises: true }); const scope = nock(client.baseUrl, { badheaders: ['authorization'], }) @@ -234,13 +233,16 @@ describe('tumblr.js', function () { describe('post request expected headers', () => { it('with body', async () => { - client.returnPromises(); + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + returnPromises: true, + }); const scope = nock(client.baseUrl, { reqheaders: { accept: 'application/json', 'user-agent': `tumblr.js/${client.version}`, - 'content-type': 'application/json', - 'content-length': '13', + 'content-type': /^multipart\/form-data;\s*boundary=/, authorization: (value) => { return [ value.startsWith('OAuth '), @@ -255,7 +257,11 @@ describe('tumblr.js', function () { }, }, }) - .post('/', '{"foo":"bar"}') + .post('/', (body) => { + return ( + /^Content-Disposition: form-data; name="foo"$/m.test(body) && /^bar$/m.test(body) + ); + }) .reply(200, { meta: {}, response: {} }); assert.isOk(await client.postRequest('/', { foo: 'bar' })); @@ -263,7 +269,11 @@ describe('tumblr.js', function () { }); it('without body', async () => { - client.returnPromises(); + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + returnPromises: true, + }); const scope = nock(client.baseUrl, { badheaders: ['content-length', 'content-type'], reqheaders: { @@ -349,6 +359,10 @@ describe('tumblr.js', function () { ); }); + it('returns undefined', function () { + assert.isUndefined(returnValue); + }); + it('invokes the callback', function () { assert.isTrue(callbackInvoked); }); From f61a0b1109e4374e0864e8aca9bec29632debb57 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:05:08 +0200 Subject: [PATCH 29/74] Update CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e9e7f19..a1e7312a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Deprecated -- Deprecated the following legacy post creation methods. Prefer NPF methods (`/posts` endpoint). +- Deprecated the following legacy post creation methods. Prefer NPF methods (`/posts` endpoint). The + deprecated methods are the following: - `createPost` - `createAudioPost` - `createChatPost` @@ -42,6 +43,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Removed - **Breaking** The `request` option has been removed. +- The dependency on the deprecated `request` library has been removed. ## [3.0.0] - 2020-07-28 From 16e4288aea24fadddb7674a6f888b6bd5b3cd2c6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:13:10 +0200 Subject: [PATCH 30/74] Clean up and deduplicate tests --- test/tumblr.test.js | 103 +++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index ff4d8e44..a6ce9135 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -324,11 +324,7 @@ describe('tumblr.js', function () { fs.readFileSync(path.join(__dirname, 'fixtures/' + httpMethod + '.json5')).toString(), ); - /** - * ### Callback - */ - - describe('returnPromises disabled', function () { + describe('with callbacks', function () { Object.entries(fixtures).forEach(function ([apiPath, data]) { describe(apiPath, function () { let callbackInvoked, requestError, requestResponse, returnValue; @@ -402,63 +398,54 @@ describe('tumblr.js', function () { }); }); - /** - * ### Promises - */ - - describe('returnPromises enabled', function () { + describe('with promises', function () { beforeEach(function () { client.returnPromises(); }); - /** @type {const} */ ([ - ['get', 'getRequest'], - ['post', 'postRequest'], - ]).forEach(function ([httpMethod, clientMethod]) { - describe('#' + clientMethod, function () { - Object.entries(fixtures).forEach(function ([apiPath, data]) { - describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - - setupNockBeforeAfter(httpMethod, data, apiPath); - - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - returnValue = client[clientMethod](apiPath, params); - // Invoke the callback when the Promise resolves or rejects - returnValue.then( - function (resp) { - callback(null, resp); - done(); - }, - function (err) { - callback(err, null); - done(); - }, - ); - }); - - it('returns a Promise', function () { - assert.isTrue(returnValue instanceof Promise); - }); - - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); + describe('#' + clientMethod, function () { + Object.entries(fixtures).forEach(function ([apiPath, data]) { + describe(apiPath, function () { + let callbackInvoked, requestError, requestResponse, returnValue; + const params = {}; + const callback = function (err, resp) { + callbackInvoked = true; + requestError = err; + requestResponse = resp; + }; + + setupNockBeforeAfter(httpMethod, data, apiPath); + + beforeEach(function (done) { + callbackInvoked = false; + requestError = false; + requestResponse = false; + + returnValue = client[clientMethod](apiPath, params); + // Invoke the callback when the Promise resolves or rejects + returnValue.then( + function (resp) { + callback(null, resp); + done(); + }, + function (err) { + callback(err, null); + done(); + }, + ); + }); + + it('returns a Promise', function () { + assert.isTrue(returnValue instanceof Promise); + }); + + it('invokes the callback', function () { + assert.isTrue(callbackInvoked); + }); + + it('gets a successful response', function () { + assert.isNull(requestError, 'err is falsy'); + assert.isDefined(requestResponse); }); }); }); From df183b18facb6602a45806ab1ebc899c86050bfb Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:44:13 +0200 Subject: [PATCH 31/74] Improve test isolation --- test/tumblr.test.js | 222 +++++++++++++++++++------------------------- 1 file changed, 94 insertions(+), 128 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index a6ce9135..d46402ca 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -115,15 +115,6 @@ describe('tumblr.js', function () { }); }); - /** @type {import('../lib/tumblr.js').Client} */ - let client; - beforeEach(function () { - client = new TumblrClient({ - ...DUMMY_CREDENTIALS, - baseUrl: DUMMY_API_URL, - }); - }); - /** * ## Default methods * @@ -131,6 +122,11 @@ describe('tumblr.js', function () { */ describe('default methods', function () { + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + }); + /** @type {const} */ ([ 'blogInfo', 'blogAvatar', @@ -176,23 +172,12 @@ describe('tumblr.js', function () { * - TumblrClient#postRequest */ - /** - * @param {'get'|'post'} httpMethod - * @param {any} data - * @param {string} apiPath - */ - function setupNockBeforeAfter(httpMethod, data, apiPath) { - before(function () { - nock(client.baseUrl)[httpMethod](apiPath).reply(data.body.meta.status, data.body).persist(); - }); - - after(function () { - nock.cleanAll(); - }); - } - it('get request expected headers', async () => { - client.returnPromises(); + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + returnPromises: true, + }); const scope = nock(client.baseUrl, { reqheaders: { accept: 'application/json', @@ -320,6 +305,30 @@ describe('tumblr.js', function () { ['post', 'postRequest'], ]).forEach(function ([httpMethod, clientMethod]) { describe('#' + clientMethod, function () { + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + }); + + /** + * @param {'get'|'post'} httpMethod + * @param {any} data + * @param {string} apiPath + */ + function setupNockBeforeAfter(httpMethod, data, apiPath) { + before(function () { + nock(client.baseUrl) + [httpMethod](apiPath) + .query(true) + .reply(data.body.meta.status, data.body) + .persist(); + }); + + after(function () { + nock.cleanAll(); + }); + } + const fixtures = JSON5.parse( fs.readFileSync(path.join(__dirname, 'fixtures/' + httpMethod + '.json5')).toString(), ); @@ -327,126 +336,78 @@ describe('tumblr.js', function () { describe('with callbacks', function () { Object.entries(fixtures).forEach(function ([apiPath, data]) { describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - setupNockBeforeAfter(httpMethod, data, apiPath); - describe('params and callback', function () { - before(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - returnValue = client[clientMethod]( - apiPath, - params, - /** @param {any} args */ - function (...args) { - callback.call(client, ...args); - done(); - }, - ); - }); - - it('returns undefined', function () { - assert.isUndefined(returnValue); - }); - - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); + it('params and callback invokes callback with a successful response', function (done) { + const returnValue = client[clientMethod](apiPath, { foo: 'bar' }, (err, resp) => { + assert.isNull(err); + assert.isDefined(resp); + done(); }); + assert.isUndefined(returnValue); }); - describe('callback only', function () { - before(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - client[clientMethod]( - apiPath, - /** @param {any} args */ - function (...args) { - callback.call(client, ...args); - done(); - }, - ); - }); - - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); + it('callback only invokes callback with a successful response', function (done) { + const returnValue = client[clientMethod](apiPath, (err, resp) => { + assert.isNull(err); + assert.isDefined(resp); + done(); }); + assert.isUndefined(returnValue); }); }); }); }); describe('with promises', function () { - beforeEach(function () { - client.returnPromises(); + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + returnPromises: true, }); - describe('#' + clientMethod, function () { - Object.entries(fixtures).forEach(function ([apiPath, data]) { - describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - - setupNockBeforeAfter(httpMethod, data, apiPath); - - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - returnValue = client[clientMethod](apiPath, params); - // Invoke the callback when the Promise resolves or rejects - returnValue.then( - function (resp) { - callback(null, resp); - done(); - }, - function (err) { - callback(err, null); - done(); - }, - ); - }); + Object.entries(fixtures).forEach(function ([apiPath, data]) { + describe(apiPath, function () { + let callbackInvoked, requestError, requestResponse, returnValue; + const params = {}; + const callback = function (err, resp) { + callbackInvoked = true; + requestError = err; + requestResponse = resp; + }; - it('returns a Promise', function () { - assert.isTrue(returnValue instanceof Promise); - }); + setupNockBeforeAfter(httpMethod, data, apiPath); - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); + beforeEach(function (done) { + callbackInvoked = false; + requestError = false; + requestResponse = false; + + returnValue = client[clientMethod](apiPath, params); + // Invoke the callback when the Promise resolves or rejects + returnValue.then( + function (resp) { + callback(null, resp); + done(); + }, + function (err) { + callback(err, null); + done(); + }, + ); + }); - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); + it('returns a Promise', function () { + assert.isTrue(returnValue instanceof Promise); + }); + + it('invokes the callback', function () { + assert.isTrue(callbackInvoked); + }); + + it('gets a successful response', function () { + assert.isNull(requestError, 'err is falsy'); + assert.isDefined(requestResponse); }); }); }); @@ -468,6 +429,11 @@ describe('tumblr.js', function () { ['post', 'addPostMethods'], ]).forEach(function ([httpMethod, clientMethod]) { describe('#' + clientMethod, function () { + const client = new TumblrClient({ + ...DUMMY_CREDENTIALS, + baseUrl: DUMMY_API_URL, + }); + const data = { meta: { status: 200, From ec7756557d49886aafac1c55005c38b92f383328 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:45:41 +0200 Subject: [PATCH 32/74] Fix broken params or callback --- lib/tumblr.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index a18c5b70..9cae88c7 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -774,12 +774,18 @@ class TumblrClient { * Performs a GET request * * @param {string} apiPath - URL path for the request - * @param {Record} [params] - query parameters + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters * @param {TumblrClientCallback} [callback] - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ - getRequest(apiPath, params = {}, callback) { + getRequest(apiPath, paramsOrCallback, callback) { + let params = paramsOrCallback; + if (typeof params === 'function') { + callback = /** @type {TumblrClientCallback} */ (params); + params = undefined; + } + const url = new URL(apiPath, this.baseUrl); if (params) { for (const [key, value] of Object.entries(params)) { @@ -914,12 +920,18 @@ class TumblrClient { * Performs a POST request * * @param {string} apiPath - URL path for the request - * @param {Record} [params] - form parameters + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters * @param {TumblrClientCallback} [callback] - request callback * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ - postRequest(apiPath, params = {}, callback) { + postRequest(apiPath, paramsOrCallback, callback) { + let params = paramsOrCallback; + if (typeof params === 'function') { + callback = /** @type {TumblrClientCallback} */ (params); + params = undefined; + } + const url = new URL(apiPath, this.baseUrl); const requestData = new Map(Object.entries(params)); From 07839cbb0c78b59c8e74034a4f87c9eca59ebd99 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:46:21 +0200 Subject: [PATCH 33/74] Fix object.entries on empty params --- lib/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 9cae88c7..06f4cdb4 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -934,7 +934,7 @@ class TumblrClient { const url = new URL(apiPath, this.baseUrl); - const requestData = new Map(Object.entries(params)); + const requestData = new Map(params ? Object.entries(params) : undefined); // Move URL search params to send them in the request body for (const [key, value] of url.searchParams.entries()) { From 4efba2aa4c9bd791776b2d8e297c05883fd168bc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 09:46:32 +0200 Subject: [PATCH 34/74] Set short timeout on tests --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ac20c9d0..e733c8be 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "format-check": "prettier --check .", "generate-docs": "rm -rf gh-pages && node_modules/.bin/jsdoc -c jsdoc.json", "gh-pages": "git subtree push --prefix=gh-pages origin gh-pages", - "test": "mocha", - "test:coverage": "nyc mocha", + "test": "mocha --timeout 100", + "test:coverage": "nyc npm run test", "test:integration": "mocha ./integration", "lint": "eslint --report-unused-disable-directives --ext 'js' --ext 'mjs' lib test integration" }, From 1b897e2401ed1b11b80d36c3daeafec23c966c76 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 12:19:01 +0200 Subject: [PATCH 35/74] Run matrix actions sequentially --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6765e1f7..1e41fa04 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,6 +14,8 @@ jobs: strategy: matrix: + fail-fast: true + max-parallel: 1 node-version: - current - lts/* From d067f10ca548974b2174ddac18a53f3f5488ad6c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 12:30:42 +0200 Subject: [PATCH 36/74] fixup! Run matrix actions sequentially --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1e41fa04..ba639958 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest strategy: + max-parallel: 1 + fail-fast: true matrix: - fail-fast: true - max-parallel: 1 node-version: - current - lts/* From b88fff52b102f44a6a2e0d4c1bfbea2d6ce9318b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 13:08:15 +0200 Subject: [PATCH 37/74] Add some test timeout, more tags to posts --- integration/read-only.mjs | 9 +++++++-- integration/write.mjs | 15 ++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/integration/read-only.mjs b/integration/read-only.mjs index 1024d36a..ec895ca6 100644 --- a/integration/read-only.mjs +++ b/integration/read-only.mjs @@ -3,6 +3,11 @@ import { Client } from 'tumblr.js'; import { assert } from 'chai'; import { test } from 'mocha'; +// Wait a bit between tests to not spam API. +beforeEach(function () { + return new Promise((resolve) => setTimeout(() => resolve(undefined), this.timeout() - 100)); +}); + describe('unauthorized requests', () => { /** @type {Client} */ let client; @@ -20,7 +25,7 @@ describe('unauthorized requests', () => { }); describe('consumer_key (api_key) only requests', () => { - /** @type {Client} */ + /** @type {import('tumblr.js').Client} */ let client; before(function () { if (!env.TUMBLR_OAUTH_CONSUMER_KEY) { @@ -30,8 +35,8 @@ describe('consumer_key (api_key) only requests', () => { client = new Client({ consumer_key: env.TUMBLR_OAUTH_CONSUMER_KEY, + returnPromises: true, }); - client.returnPromises(); }); ['staff', 'staff.tumblr.com', 't:0aY0xL2Fi1OFJg4YxpmegQ'].forEach((blogIdentifier) => { diff --git a/integration/write.mjs b/integration/write.mjs index 7a792aca..bc65c544 100644 --- a/integration/write.mjs +++ b/integration/write.mjs @@ -42,6 +42,11 @@ describe('oauth1 write requests', () => { blogName = userResp.user.blogs[0].name; }); + // Wait a bit between tests to not spam API. + beforeEach(function () { + return new Promise((resolve) => setTimeout(() => resolve(undefined), this.timeout() - 100)); + }); + test('creates a text post', async () => { assert.isOk( await client.createPost(blogName, { @@ -49,7 +54,7 @@ describe('oauth1 write requests', () => { format: 'markdown', title: `Automated test post ${new Date().toISOString()}`, body: 'This post was automatically generated by the tumblr.js tests.\n\n[The official JavaScript client library for the Tumblr API.](https://github.com/tumblr/tumblr.js)', - tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version},test-legacy-text`, }), ); }); @@ -62,7 +67,7 @@ describe('oauth1 write requests', () => { type: 'photo', caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', - tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version},test-legacy-photo-data`, data: data, }); assert.isOk(res); @@ -75,7 +80,7 @@ describe('oauth1 write requests', () => { type: 'photo', caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', - tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version},test-legacy-photo-data[]`, data: [data, data], }); assert.isOk(res); @@ -90,7 +95,7 @@ describe('oauth1 write requests', () => { type: 'photo', caption: `Arches National Park || Automated test post ${new Date().toISOString()}`, link: 'https://openverse.org/en-gb/image/38b9b781-390f-4fc4-929d-0ecb4a2985e3', - tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version},test-legacy-photo-data64`, data64: data, }); assert.isOk(res); @@ -103,7 +108,7 @@ describe('oauth1 write requests', () => { const res = await client.createPost(blogName, { type: 'audio', caption: `Multiple Dog Barks (King Charles Spaniel) || Automated test post ${new Date().toISOString()}`, - tags: `tumblr.js-test,tumblr.js-version-${client.version}`, + tags: `tumblr.js-test,tumblr.js-version-${client.version},test-legacy-audio`, data: data, }); assert.isOk(res); From bcfc1279e21e8adec1643e6532cd19cd0caaf2ea Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 13:13:39 +0200 Subject: [PATCH 38/74] CI: Run lint once --- .github/workflows/ci.yaml | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ba639958..18ca2b18 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,32 @@ on: - master jobs: + lint: + name: Linting + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: lts/* + cache: npm + + - name: Install dependencies + run: | + npm ci + + - name: Run lint + run: | + npm run lint + + - name: Check formatting + run: | + npm run format-check + tests: name: Testing with Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest @@ -39,11 +65,6 @@ jobs: run: | npm run build --if-present - - name: Run lint - run: | - npm run lint - npm run format-check - - name: Run tests run: | npm run test:coverage From 4e6c8c98ba3fd2659acad7ad290c27188e8e8dc0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 13:44:18 +0200 Subject: [PATCH 39/74] Split integration out to manual trigger on branches --- .github/workflows/ci.yaml | 9 ------ .github/workflows/integration.yaml | 47 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/integration.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 18ca2b18..533b0d6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -68,12 +68,3 @@ jobs: - name: Run tests run: | npm run test:coverage - - - name: Run integration tests - env: - TUMBLR_OAUTH_CONSUMER_KEY: ${{ secrets.TUMBLR_OAUTH_CONSUMER_KEY }} - TUMBLR_OAUTH_CONSUMER_SECRET: ${{ secrets.TUMBLR_OAUTH_CONSUMER_SECRET }} - TUMBLR_OAUTH_TOKEN: ${{ secrets.TUMBLR_OAUTH_TOKEN }} - TUMBLR_OAUTH_TOKEN_SECRET: ${{ secrets.TUMBLR_OAUTH_TOKEN_SECRET }} - run: | - npm run test:integration diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 00000000..bfb41211 --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,47 @@ +name: Integration tests + +on: + workflow_dispatch: + push: + branches: + - main + - master + +jobs: + tests: + name: Integration testing with Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: + - current + - lts/* + - lts/-1 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: | + npm ci + + - name: Build + run: | + npm run build --if-present + + - name: Run integration tests + env: + TUMBLR_OAUTH_CONSUMER_KEY: ${{ secrets.TUMBLR_OAUTH_CONSUMER_KEY }} + TUMBLR_OAUTH_CONSUMER_SECRET: ${{ secrets.TUMBLR_OAUTH_CONSUMER_SECRET }} + TUMBLR_OAUTH_TOKEN: ${{ secrets.TUMBLR_OAUTH_TOKEN }} + TUMBLR_OAUTH_TOKEN_SECRET: ${{ secrets.TUMBLR_OAUTH_TOKEN_SECRET }} + run: | + npm run test:integration From 344ccf01dfb993f8c63b22cede5d6f551c514cad Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 13:45:47 +0200 Subject: [PATCH 40/74] Parallel jobs --- .github/workflows/ci.yaml | 1 - .github/workflows/integration.yaml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 533b0d6d..ed8aaf85 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,6 @@ jobs: runs-on: ubuntu-latest strategy: - max-parallel: 1 fail-fast: true matrix: node-version: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index bfb41211..6c0d8a5f 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: true + max-parallel: 1 matrix: node-version: - current From 4344c59ab6b250d2a370faf9908105fe7031e627 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:09:14 +0200 Subject: [PATCH 41/74] Remove legacy create*Post methods --- CHANGELOG.md | 18 ++-- lib/tumblr.js | 243 ++++---------------------------------------- test/tumblr.test.js | 7 -- 3 files changed, 29 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e7312a..1dd8ec03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,16 +22,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Deprecated -- Deprecated the following legacy post creation methods. Prefer NPF methods (`/posts` endpoint). The - deprecated methods are the following: +- The following legacy post methods are deprecated. Prefer NPF methods (`/posts` endpoint) - `createPost` - - `createAudioPost` - - `createChatPost` - - `createLinkPost` - - `createPhotoPost` - - `createQuotePost` - - `createTextPost` - - `createVideoPost` - `editPost` - `reblogPost` @@ -42,6 +34,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Removed +- **Breaking** The following legacy post creation methods have been removed. + - `createAudioPost`: use `ceatePost` with `{type: "audio"}`. + - `createChatPost`: use `ceatePost` with `{type: "chat"}`. + - `createLinkPost`: use `ceatePost` with `{type: "link"}`. + - `createPhotoPost`: use `ceatePost` with `{type: "photo"}`. + - `createQuotePost`: use `ceatePost` with `{type: "quote"}`. + - `createTextPost`: use `ceatePost` with `{type: "text"}`. + - `createVideoPost`: use `ceatePost` with `{type: "video"}`. - **Breaking** The `request` option has been removed. - The dependency on the deprecated `request` library has been removed. diff --git a/lib/tumblr.js b/lib/tumblr.js index 06f4cdb4..eae51da8 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -14,15 +14,11 @@ const http = require('node:http'); const https = require('node:https'); const { URL } = require('node:url'); const oauth = require('oauth'); -const keys = require('lodash/keys'); -const intersection = require('lodash/intersection'); const extend = require('lodash/extend'); const reduce = require('lodash/reduce'); -const partial = require('lodash/partial'); const zipObject = require('lodash/zipObject'); const isString = require('lodash/isString'); const isFunction = require('lodash/isFunction'); -const isArray = require('lodash/isArray'); const isPlainObject = require('lodash/isPlainObject'); const CLIENT_VERSION = '4.0.0-alpha.0'; @@ -223,24 +219,6 @@ const API_METHODS = { }, POST: { - /** - * Creates a post on the given blog. - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} - * @method createPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - createPost: '/v2/blog/:blogIdentifier/post', - /** * Edits a given post * @@ -414,50 +392,6 @@ function promisifyRequest(requestMethod) { }; } -/** - * Wraps createPost to specify `type` and validate the parameters - * - * @param {string} type - post type - * @param {Function} [validate] - returns `true` if the parameters validate - * - * @return {Function} wrapped function - * - */ -function wrapCreatePost(type, validate) { - /** @this {TumblrClient} */ - return function (blogIdentifier, params, callback) { - params = extend({ type: type }, params); - - if (isArray(validate)) { - validate = partial( - function (params, requireKeys) { - if (requireKeys.length) { - const keyIntersection = intersection(keys(params), requireKeys); - if (requireKeys.length === 1 && !keyIntersection.length) { - throw new Error('Missing required field: ' + requireKeys[0]); - } else if (!keyIntersection.length) { - throw new Error('Missing one of: ' + requireKeys.join(', ')); - } else if (keyIntersection.length > 1) { - throw new Error('Can only use one of: ' + requireKeys.join(', ')); - } - } - return true; - }, - params, - validate, - ); - } - - if (isFunction(validate)) { - if (!validate(params)) { - throw new Error('Error validating parameters'); - } - } - - return this.createPost(blogIdentifier, params, callback); - }; -} - /** * @typedef Options * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. @@ -609,161 +543,6 @@ class TumblrClient { this.addGetMethods(API_METHODS.GET); this.addPostMethods(API_METHODS.POST); - /** - * Creates a text post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#ptext-posts|API docs} - * - * @method createTextPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} [params.title] - post title text - * @param {string} params.body - post body text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createTextPost = wrapCreatePost('text', ['body']); - - /** - * Creates a photo post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#pphoto-posts|API docs} - * - * @method createPhotoPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} params.source - image source URL - * @param {Stream|Array} params.data - an image or array of images - * @param {string} params.data64 - base64-encoded image data - * @param {string} [params.caption] - post caption text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createPhotoPost = wrapCreatePost('photo', ['data', 'data64', 'source']); - - /** - * Creates a quote post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#pquote-posts|API docs} - * - * @method createQuotePost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} params.quote - quote text - * @param {string} [params.source] - quote source - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createQuotePost = wrapCreatePost('quote', ['quote']); - - /** - * Creates a link post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#plink-posts|API docs} - * - * @method createLinkPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} [params.title] - post title text - * @param {string} params.url - the link URL - * @param {string} [params.thumbnail] - the URL of an image to use as the thumbnail - * @param {string} [params.excerpt] - an excerpt from the page the link points to - * @param {string} [params.author] - the name of the author of the page the link points to - * @param {string} [params.description] - post caption text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createLinkPost = wrapCreatePost('link', ['url']); - - /** - * Creates a chat post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#pchat-posts|API docs} - * - * @method createChatPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} [params.title] - post title text - * @param {string} params.conversation - chat text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createChatPost = wrapCreatePost('chat', ['conversation']); - - /** - * Creates an audio post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#paudio-posts|API docs} - * - * @method createAudioPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} params.external_url - image source URL - * @param {Stream} params.data - an audio file - * @param {string} [params.caption] - post caption text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createAudioPost = wrapCreatePost('audio', ['data', 'data64', 'external_url']); - - /** - * Creates a video post on the given blog - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#pvideo-posts|API docs} - * - * @method createVideoPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {string} params.embed - embed code or a video URL - * @param {Stream} params.data - a video file - * @param {string} [params.caption] - post caption text - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - this.createVideoPost = wrapCreatePost('video', ['data', 'data64', 'embed']); - // Enable Promise mode if (options?.returnPromises) { this.returnPromises(); @@ -920,8 +699,8 @@ class TumblrClient { * Performs a POST request * * @param {string} apiPath - URL path for the request - * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - request callback + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] * * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used */ @@ -1071,6 +850,24 @@ class TumblrClient { this[methodName] = createFunction(methodName, namedParams, methodBody); } + + /** + * Creates a post on the given blog. + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} + * @method createPost + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used + */ + createPost(blogIdentifier, paramsOrCallback, callback) { + return this.postRequest(`/v2/blog/${blogIdentifier}/post`, paramsOrCallback, callback); + } } /** diff --git a/test/tumblr.test.js b/test/tumblr.test.js index d46402ca..6c59afd0 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -149,13 +149,6 @@ describe('tumblr.js', function () { 'unfollowBlog', 'likePost', 'unlikePost', - 'createTextPost', - 'createPhotoPost', - 'createQuotePost', - 'createLinkPost', - 'createChatPost', - 'createAudioPost', - 'createVideoPost', ]).forEach(function (methodName) { it('has #' + methodName, function () { assert.isFunction(client[methodName]); From fd3097787bd97de192ce8036ad9d27a91b956130 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:19:25 +0200 Subject: [PATCH 42/74] Clean up signatures, start moving functions into class --- lib/tumblr.js | 117 +++++++++++++++++--------------------------------- 1 file changed, 39 insertions(+), 78 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index eae51da8..6196bede 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -35,9 +35,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogInfo: '/v2/blog/:blogIdentifier/info', @@ -51,9 +49,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogAvatar: '/v2/blog/:blogIdentifier/avatar/:size', @@ -66,9 +62,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogLikes: '/v2/blog/:blogIdentifier/likes', @@ -81,9 +75,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogFollowers: '/v2/blog/:blogIdentifier/followers', @@ -96,8 +88,6 @@ const API_METHODS = { * @param {string} [type] - filters returned posts to the specified type * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @memberof TumblrClient */ blogPosts: '/v2/blog/:blogIdentifier/posts/:type', @@ -110,9 +100,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogQueue: '/v2/blog/:blogIdentifier/posts/queue', @@ -125,9 +113,7 @@ const API_METHODS = { * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogDrafts: '/v2/blog/:blogIdentifier/posts/draft', @@ -140,9 +126,7 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ blogSubmissions: '/v2/blog/:blogIdentifier/posts/submission', @@ -154,9 +138,7 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ userInfo: '/v2/user/info', @@ -168,9 +150,7 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ userDashboard: '/v2/user/dashboard', @@ -182,9 +162,7 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ userFollowing: '/v2/user/following', @@ -196,9 +174,7 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ userLikes: '/v2/user/likes', @@ -211,31 +187,12 @@ const API_METHODS = { * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ taggedPosts: ['/v2/tagged', ['tag']], }, POST: { - /** - * Edits a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @method editPost - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient - */ - editPost: '/v2/blog/:blogIdentifier/post/edit', - /** * Reblogs a given post * @@ -247,9 +204,7 @@ const API_METHODS = { * @param {Object} params - parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ reblogPost: '/v2/blog/:blogIdentifier/post/reblog', @@ -263,9 +218,7 @@ const API_METHODS = { * @param {Object} params.id - ID of the post to delete * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ deletePost: ['/v2/blog/:blogIdentifier/post/delete', ['id']], @@ -278,9 +231,7 @@ const API_METHODS = { * @param {Object} params.url - URL of the blog to follow * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ followBlog: ['/v2/user/follow', ['url']], @@ -293,9 +244,7 @@ const API_METHODS = { * @param {Object} params.url - URL of the blog to unfollow * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ unfollowBlog: ['/v2/user/unfollow', ['url']], @@ -309,9 +258,7 @@ const API_METHODS = { * @param {Object} params.reblog_key - Reblog key for the post ID * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ likePost: ['/v2/user/like', ['id', 'reblog_key']], @@ -325,9 +272,7 @@ const API_METHODS = { * @param {Object} params.reblog_key - Reblog key for the post ID * @param {TumblrClientCallback} [callback] - invoked when the request completes * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used - * - * @memberof TumblrClient + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ unlikePost: ['/v2/user/unlike', ['id', 'reblog_key']], }, @@ -556,7 +501,7 @@ class TumblrClient { * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters * @param {TumblrClientCallback} [callback] - request callback * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ getRequest(apiPath, paramsOrCallback, callback) { let params = paramsOrCallback; @@ -581,6 +526,8 @@ class TumblrClient { * @param {null|RequestData} data * @param {TumblrClientCallback} [callback] * + * @returns {Promise|undefined} + * * @private */ makeRequest(url, method, data, callback) { @@ -693,6 +640,7 @@ class TumblrClient { }); request.end(); + return; } /** @@ -702,7 +650,7 @@ class TumblrClient { * @param {Record|TumblrClientCallback} [paramsOrCallback] * @param {TumblrClientCallback} [callback] * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ postRequest(apiPath, paramsOrCallback, callback) { let params = paramsOrCallback; @@ -863,11 +811,26 @@ class TumblrClient { * @param {Record|TumblrClientCallback} [paramsOrCallback] * @param {TumblrClientCallback} [callback] * - * @return {Request|Promise} Request object, or Promise if {@link returnPromises} was used + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ createPost(blogIdentifier, paramsOrCallback, callback) { return this.postRequest(`/v2/blog/${blogIdentifier}/post`, paramsOrCallback, callback); } + + /** + * Edits a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + editPost(blogIdentifier, paramsOrCallback, callback) { + return this.postRequest(`/v2/blog/${blogIdentifier}/post/edit`, paramsOrCallback, callback); + } } /** @@ -887,7 +850,6 @@ module.exports = { /** * Passthrough for the {@link TumblrClient} class * - * @memberof tumblr * @see {@link TumblrClient} * @type {typeof TumblrClient} */ @@ -900,7 +862,6 @@ module.exports = { * * @return {TumblrClient} {@link TumblrClient} instance * - * @memberof tumblr * @see {@link TumblrClient} */ createClient: function (options) { From 6883de4475aac6849788420acb51289f75bd56fd Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:20:32 +0200 Subject: [PATCH 43/74] Remove redundant @method tags --- lib/tumblr.js | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 6196bede..8d230900 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -29,8 +29,6 @@ const API_METHODS = { /** * Gets information about a given blog * - * @method blogInfo - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -42,8 +40,6 @@ const API_METHODS = { /** * Gets the avatar URL for a blog * - * @method blogAvatar - * * @param {string} blogIdentifier - blog name or URL * @param {number} [size] - avatar size, in pixels * @param {Object} [params] - optional data sent with the request @@ -56,8 +52,6 @@ const API_METHODS = { /** * Gets the likes for a blog * - * @method blogLikes - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -69,8 +63,6 @@ const API_METHODS = { /** * Gets the followers for a blog * - * @method blogFollowers - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -82,8 +74,6 @@ const API_METHODS = { /** * Gets a list of posts for a blog * - * @method blogPosts - * * @param {string} blogIdentifier - blog name or URL * @param {string} [type] - filters returned posts to the specified type * @param {Object} [params] - optional data sent with the request @@ -94,8 +84,6 @@ const API_METHODS = { /** * Gets the queue for a blog * - * @method blogQueue - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -107,8 +95,6 @@ const API_METHODS = { /** * Gets the drafts for a blog * - * @method blogDrafts - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional data sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -120,8 +106,6 @@ const API_METHODS = { /** * Gets the submissions for a blog * - * @method blogSubmissions - * * @param {string} blogIdentifier - blog name or URL * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -133,8 +117,6 @@ const API_METHODS = { /** * Gets information about the authenticating user and their blogs * - * @method userInfo - * * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * @@ -145,8 +127,6 @@ const API_METHODS = { /** * Gets the dashboard posts for the authenticating user * - * @method userDashboard - * * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * @@ -157,8 +137,6 @@ const API_METHODS = { /** * Gets the blogs the authenticating user follows * - * @method userFollowing - * * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * @@ -169,8 +147,6 @@ const API_METHODS = { /** * Gets the likes for the authenticating user * - * @method userLikes - * * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes * @@ -181,8 +157,6 @@ const API_METHODS = { /** * Gets posts tagged with the specified tag * - * @method taggedPosts - * * @param {string} [tag] - tag to search for * @param {Object} [params] - optional parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -198,8 +172,6 @@ const API_METHODS = { * * @deprecated Legacy post creation methods are deprecated. Use NPF methods. * - * @method reblogPost - * * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -211,8 +183,6 @@ const API_METHODS = { /** * Deletes a given post * - * @method deletePost - * * @param {string} blogIdentifier - blog name or URL * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to delete @@ -225,8 +195,6 @@ const API_METHODS = { /** * Follows a blog as the authenticating user * - * @method followBlog - * * @param {Object} params - parameters sent with the request * @param {Object} params.url - URL of the blog to follow * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -238,8 +206,6 @@ const API_METHODS = { /** * Unfollows a blog as the authenticating user * - * @method unfollowBlog - * * @param {Object} params - parameters sent with the request * @param {Object} params.url - URL of the blog to unfollow * @param {TumblrClientCallback} [callback] - invoked when the request completes @@ -251,8 +217,6 @@ const API_METHODS = { /** * Likes a post as the authenticating user * - * @method likePost - * * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to like * @param {Object} params.reblog_key - Reblog key for the post ID @@ -265,8 +229,6 @@ const API_METHODS = { /** * Unlikes a post as the authenticating user * - * @method unlikePost - * * @param {Object} params - parameters sent with the request * @param {Object} params.id - ID of the post to unlike * @param {Object} params.reblog_key - Reblog key for the post ID @@ -805,7 +767,6 @@ class TumblrClient { * @deprecated Legacy post creation methods are deprecated. Use NPF methods. * * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} - * @method createPost * * @param {string} blogIdentifier - blog name or URL * @param {Record|TumblrClientCallback} [paramsOrCallback] From 140472ccd18a93bd59126f137a71646b83007f42 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:25:40 +0200 Subject: [PATCH 44/74] Move like/unlike to class --- lib/tumblr.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 8d230900..087f98b6 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -213,30 +213,6 @@ const API_METHODS = { * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ unfollowBlog: ['/v2/user/unfollow', ['url']], - - /** - * Likes a post as the authenticating user - * - * @param {Object} params - parameters sent with the request - * @param {Object} params.id - ID of the post to like - * @param {Object} params.reblog_key - Reblog key for the post ID - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - likePost: ['/v2/user/like', ['id', 'reblog_key']], - - /** - * Unlikes a post as the authenticating user - * - * @param {Object} params - parameters sent with the request - * @param {Object} params.id - ID of the post to unlike - * @param {Object} params.reblog_key - Reblog key for the post ID - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unlikePost: ['/v2/user/unlike', ['id', 'reblog_key']], }, }; @@ -792,6 +768,30 @@ class TumblrClient { editPost(blogIdentifier, paramsOrCallback, callback) { return this.postRequest(`/v2/blog/${blogIdentifier}/post/edit`, paramsOrCallback, callback); } + + /** + * Likes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + likePost(params, callback) { + return this.postRequest('/v2/user/like', params, callback); + } + + /** + * Unlikes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unlikePost(params, callback) { + return this.postRequest('/v2/user/unlike', params, callback); + } } /** From f45c2abb9fd2ab72dd0660f2ef57e9be7ce029d9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:28:24 +0200 Subject: [PATCH 45/74] Move follow/unfollow to class --- lib/tumblr.js | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 087f98b6..a07b8854 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -191,28 +191,6 @@ const API_METHODS = { * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ deletePost: ['/v2/blog/:blogIdentifier/post/delete', ['id']], - - /** - * Follows a blog as the authenticating user - * - * @param {Object} params - parameters sent with the request - * @param {Object} params.url - URL of the blog to follow - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - followBlog: ['/v2/user/follow', ['url']], - - /** - * Unfollows a blog as the authenticating user - * - * @param {Object} params - parameters sent with the request - * @param {Object} params.url - URL of the blog to unfollow - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unfollowBlog: ['/v2/user/unfollow', ['url']], }, }; @@ -792,6 +770,30 @@ class TumblrClient { unlikePost(params, callback) { return this.postRequest('/v2/user/unlike', params, callback); } + + /** + * Follows a blog as the authenticating user + * + * @param {{url: string}|{email:string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + followBlog(params, callback) { + return this.postRequest('/v2/user/follow', params, callback); + } + + /** + * Unfollows a blog as the authenticating user + * + * @param {{url: string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unfollowBlog(params, callback) { + return this.postRequest('/v2/user/unfollow', params, callback); + } } /** From 1bdac86327ea364ea8973d6d468341c12a4379e4 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:33:04 +0200 Subject: [PATCH 46/74] Move deletePost into class --- lib/tumblr.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index a07b8854..1edb81c1 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -179,18 +179,6 @@ const API_METHODS = { * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ reblogPost: '/v2/blog/:blogIdentifier/post/reblog', - - /** - * Deletes a given post - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {Object} params.id - ID of the post to delete - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - deletePost: ['/v2/blog/:blogIdentifier/post/delete', ['id']], }, }; @@ -794,6 +782,19 @@ class TumblrClient { unfollowBlog(params, callback) { return this.postRequest('/v2/user/unfollow', params, callback); } + + /** + * Deletes a given post + * + * @param {string} blogIdentifier - blog name or URL + * @param {{id:string}} params + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + deletePost(blogIdentifier, params, callback) { + return this.postRequest('/v2/blog/:blogIdentifier/post/delete', params, callback); + } } /** From fe2a956c3f6bab77afb1f1f521d1b3ae3c8aeb14 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:36:48 +0200 Subject: [PATCH 47/74] Move reblogPost into class --- lib/tumblr.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 1edb81c1..4bc137da 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -165,21 +165,6 @@ const API_METHODS = { */ taggedPosts: ['/v2/tagged', ['tag']], }, - - POST: { - /** - * Reblogs a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - reblogPost: '/v2/blog/:blogIdentifier/post/reblog', - }, }; /** @@ -795,6 +780,21 @@ class TumblrClient { deletePost(blogIdentifier, params, callback) { return this.postRequest('/v2/blog/:blogIdentifier/post/delete', params, callback); } + + /** + * Reblogs a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + reblogPost(blogIdentifier, params, callback) { + return this.postRequest(`/v2/blog/${blogIdentifier}/post/reblog`, params, callback); + } } /** From fcea948c817c730dbfa39314f7dbc6ed4f436358 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 14:37:19 +0200 Subject: [PATCH 48/74] Remove addPostMethods --- lib/tumblr.js | 9 --------- test/tumblr.test.js | 9 ++++----- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 4bc137da..1e9cfbdb 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -375,7 +375,6 @@ class TumblrClient { : null; this.addGetMethods(API_METHODS.GET); - this.addPostMethods(API_METHODS.POST); // Enable Promise mode if (options?.returnPromises) { @@ -581,14 +580,6 @@ class TumblrClient { addGetMethods(methods) { this.addMethods(methods, 'GET'); } - /** - * Adds POST methods to the client - * - * @param {Object} methods - mapping of method names to endpoints - */ - addPostMethods(methods) { - this.addMethods(methods, 'POST'); - } /** * Adds methods to the client diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 6c59afd0..28259a01 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -414,13 +414,12 @@ describe('tumblr.js', function () { * Test the methods that add methods to the client * * - TumblrClient#addGetMethods - * - TumblrClient#addPostMethods */ - /** @type {const} */ ([ - ['get', 'addGetMethods'], - ['post', 'addPostMethods'], - ]).forEach(function ([httpMethod, clientMethod]) { + /** @type {const} */ ([['get', 'addGetMethods']]).forEach(function ([ + httpMethod, + clientMethod, + ]) { describe('#' + clientMethod, function () { const client = new TumblrClient({ ...DUMMY_CREDENTIALS, From 8be977fb45dc24f449945aeb81aa4188ddb2539f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 19:22:48 +0200 Subject: [PATCH 49/74] Move blog methods into class --- lib/tumblr.js | 229 ++++++++++++++++++++++++++++---------------------- 1 file changed, 130 insertions(+), 99 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 1e9cfbdb..9208163f 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -26,94 +26,6 @@ const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash const API_METHODS = { GET: { - /** - * Gets information about a given blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogInfo: '/v2/blog/:blogIdentifier/info', - - /** - * Gets the avatar URL for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {number} [size] - avatar size, in pixels - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogAvatar: '/v2/blog/:blogIdentifier/avatar/:size', - - /** - * Gets the likes for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogLikes: '/v2/blog/:blogIdentifier/likes', - - /** - * Gets the followers for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogFollowers: '/v2/blog/:blogIdentifier/followers', - - /** - * Gets a list of posts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {string} [type] - filters returned posts to the specified type - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - */ - blogPosts: '/v2/blog/:blogIdentifier/posts/:type', - - /** - * Gets the queue for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogQueue: '/v2/blog/:blogIdentifier/posts/queue', - - /** - * Gets the drafts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogDrafts: '/v2/blog/:blogIdentifier/posts/draft', - - /** - * Gets the submissions for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogSubmissions: '/v2/blog/:blogIdentifier/posts/submission', - /** * Gets information about the authenticating user and their blogs * @@ -153,17 +65,6 @@ const API_METHODS = { * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ userLikes: '/v2/user/likes', - - /** - * Gets posts tagged with the specified tag - * - * @param {string} [tag] - tag to search for - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - taggedPosts: ['/v2/tagged', ['tag']], }, }; @@ -240,6 +141,11 @@ class TumblrClient { /** * @typedef {Map|string>} RequestData * + * @typedef {'text'|'quote'|'link'|'answer'|'video'|'audio'|'photo'|'chat'} PostType + * + * @typedef {'text'|'raw'} PostFormatFilter + * + * * @typedef {{readonly auth:'none'}} NoneAuthCredentials * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials @@ -786,6 +692,131 @@ class TumblrClient { reblogPost(blogIdentifier, params, callback) { return this.postRequest(`/v2/blog/${blogIdentifier}/post/reblog`, params, callback); } + + /** + * Gets information about a given blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogInfo(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest(`/v2/blog/${blogIdentifier}/info`, paramsOrCallback, callback); + } + + /** + * Gets the likes for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogLikes(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest(`/v2/blog/${blogIdentifier}/likes`, paramsOrCallback, callback); + } + + /** + * Gets the followers for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogFollowers(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest(`/v2/blog/${blogIdentifier}/followers`, paramsOrCallback, callback); + } + + /** + * Gets a list of posts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + */ + blogPosts(blogIdentifier, paramsOrCallback, callback) { + let type = undefined; + if (paramsOrCallback && typeof paramsOrCallback !== 'function') { + type = paramsOrCallback.type; + delete paramsOrCallback.type; + } + return this.getRequest(`/v2/blog/${blogIdentifier}/posts/${type}`, paramsOrCallback, callback); + } + + /** + * Gets the queue for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogQueue(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest(`/v2/blog/${blogIdentifier}/posts/queue`, paramsOrCallback, callback); + } + + /** + * Gets the drafts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogDrafts(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest(`/v2/blog/${blogIdentifier}/posts/draft`, paramsOrCallback, callback); + } + + /** + * Gets the submissions for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogSubmissions(blogIdentifier, paramsOrCallback, callback) { + return this.getRequest( + `/v2/blog/${blogIdentifier}/posts/submission`, + paramsOrCallback, + callback, + ); + } + + /** + * Gets the avatar URL for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{size?: 16|24|30|40|48|64|96|128|512}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [maybeCallback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogAvatar(blogIdentifier, paramsOrCallback, maybeCallback) { + const size = typeof paramsOrCallback === 'function' ? undefined : paramsOrCallback?.size; + const callback = typeof paramsOrCallback === 'function' ? paramsOrCallback : maybeCallback; + return this.getRequest(`/v2/blog/${blogIdentifier}/avatar/${size}`, undefined, callback); + } + + /** + * Gets posts tagged with the specified tag + * + * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + taggedPosts(params, callback) { + return this.getRequest('/v2/tagged', params, callback); + } } /** From c3def6edf75d7dd161e65d49e75b45db2a011b00 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 19:30:36 +0200 Subject: [PATCH 50/74] Move user methods into class --- lib/tumblr.js | 113 +++++++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 9208163f..0a9fbe4f 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -24,50 +24,6 @@ const isPlainObject = require('lodash/isPlainObject'); const CLIENT_VERSION = '4.0.0-alpha.0'; const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash -const API_METHODS = { - GET: { - /** - * Gets information about the authenticating user and their blogs - * - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userInfo: '/v2/user/info', - - /** - * Gets the dashboard posts for the authenticating user - * - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userDashboard: '/v2/user/dashboard', - - /** - * Gets the blogs the authenticating user follows - * - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userFollowing: '/v2/user/following', - - /** - * Gets the likes for the authenticating user - * - * @param {Object} [params] - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userLikes: '/v2/user/likes', - }, -}; - /** * Creates a named function with the desired signature * @@ -137,6 +93,16 @@ function promisifyRequest(requestMethod) { * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. */ +/** + * Handles the response from a client reuest + * + * @callback TumblrClientCallback + * @param {?Error} err - error message + * @param {?Object} resp - response body + * @param {?http.IncomingMessage} [response] - raw response + * @returns {void} + */ + class TumblrClient { /** * @typedef {Map|string>} RequestData @@ -280,8 +246,6 @@ class TumblrClient { ) : null; - this.addGetMethods(API_METHODS.GET); - // Enable Promise mode if (options?.returnPromises) { this.returnPromises(); @@ -806,6 +770,53 @@ class TumblrClient { return this.getRequest(`/v2/blog/${blogIdentifier}/avatar/${size}`, undefined, callback); } + /** + * Gets information about the authenticating user and their blogs + * + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userInfo(callback) { + return this.getRequest('/v2/user/info', undefined, callback); + } + + /** + * Gets the dashboard posts for the authenticating user + * + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userDashboard(paramsOrCallback, callback) { + return this.getRequest('/v2/user/dashboard', paramsOrCallback, callback); + } + + /** + * Gets the blogs the authenticating user follows + * + * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userFollowing(paramsOrCallback, callback) { + return this.getRequest('/v2/user/following', paramsOrCallback, callback); + } + + /** + * Gets the likes for the authenticating user + * + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userLikes(paramsOrCallback, callback) { + return this.getRequest('/v2/user/likes', paramsOrCallback, callback); + } + /** * Gets posts tagged with the specified tag * @@ -819,16 +830,6 @@ class TumblrClient { } } -/** - * Handles the response from a client reuest - * - * @callback TumblrClientCallback - * @param {?Error} err - error message - * @param {?Object} resp - response body - * @param {?http.IncomingMessage} [response] - raw response - * @returns {void} - */ - /* * Please, enjoy our luxurious exports. */ From 0fac651c4a9355962ede41a2f40f10bc5dfdb072 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 19:34:05 +0200 Subject: [PATCH 51/74] Remove addMethods --- lib/tumblr.js | 135 -------------------------------------------- test/tumblr.test.js | 111 ------------------------------------ 2 files changed, 246 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 0a9fbe4f..bf997194 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -14,38 +14,10 @@ const http = require('node:http'); const https = require('node:https'); const { URL } = require('node:url'); const oauth = require('oauth'); -const extend = require('lodash/extend'); -const reduce = require('lodash/reduce'); -const zipObject = require('lodash/zipObject'); -const isString = require('lodash/isString'); -const isFunction = require('lodash/isFunction'); -const isPlainObject = require('lodash/isPlainObject'); const CLIENT_VERSION = '4.0.0-alpha.0'; const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash -/** - * Creates a named function with the desired signature - * - * @param {string} name - function name - * @param {Array} args - array of argument names - * @param {Function} fn - function that contains the logic that should run - * - * @return {Function} a named function that takes the desired arguments - * - * @private - */ -function createFunction(name, args, fn) { - return new Function( - 'body', - 'return function ' + - name + - '(' + - args.join(', ') + - ') { return body.apply(this, arguments); };', - )(fn); -} - /** * Take a callback-based function and returns a Promise instead * @@ -442,113 +414,6 @@ class TumblrClient { this.postRequest = promisifyRequest(this.postRequest); } - /** - * Adds GET methods to the client - * - * @param {Object} methods - mapping of method names to endpoints - */ - addGetMethods(methods) { - this.addMethods(methods, 'GET'); - } - - /** - * Adds methods to the client - * - * @this {TumblrClient} - * @param {Object} methods - mapping of method names to endpoints. Endpoints can be a string or an - * array of format `[apiPathString, requireParamsArray]` - * @param {'GET'|'POST'} [requestType] - the request type or a function that makes the request - * - * @private - */ - addMethods(methods, requestType) { - let apiPath, paramNames; - for (const methodName in methods) { - apiPath = methods[methodName]; - if (isString(apiPath)) { - paramNames = []; - } else if (isPlainObject(apiPath)) { - paramNames = apiPath.paramNames || []; - apiPath = apiPath.path; - } else { - paramNames = apiPath[1] || []; - apiPath = apiPath[0]; - } - this.addMethod(methodName, apiPath, paramNames, requestType); - } - } - - /** - * Adds a request method to the client - * - * @param {string} methodName - the name of the method - * @param {string} apiPath - the API route, which uses any colon-prefixed segments as arguments - * @param {ReadonlyArray} paramNames - ordered list of required request parameters used as arguments - * @param {'GET'|'POST'} [requestType] - the request type or a function that makes the request - * - * @private - */ - addMethod(methodName, apiPath, paramNames, requestType) { - const apiPathSplit = apiPath.split('/'); - const apiPathParamsCount = apiPath.split(/\/:[^/]+/).length - 1; - - const buildApiPath = function (args) { - let pathParamIndex = 0; - return reduce( - apiPathSplit, - function (apiPath, apiPathChunk) { - // Parse arguments in the path - if (apiPathChunk[0] === ':') { - apiPathChunk = args[pathParamIndex++]; - } - - if (apiPathChunk) { - return apiPath + '/' + apiPathChunk; - } else { - return apiPath; - } - }, - '', - ); - }; - - const namedParams = (apiPath.match(/\/:[^/]+/g) || []) - .map(function (param) { - return param.substr(2); - }) - .concat(paramNames, 'params', 'callback'); - - const methodBody = - /** @this {TumblrClient} */ - function () { - const argsLength = arguments.length; - const args = new Array(argsLength); - for (let i = 0; i < argsLength; i++) { - args[i] = arguments[i]; - } - - const requiredParamsStart = apiPathParamsCount; - const requiredParamsEnd = requiredParamsStart + paramNames.length; - const requiredParamArgs = args.slice(requiredParamsStart, requiredParamsEnd); - - // Callback is at the end - const callback = isFunction(args[args.length - 1]) ? args.pop() : null; - - // Required Parmas - const params = zipObject(paramNames, requiredParamArgs); - extend(params, isPlainObject(args[args.length - 1]) ? args.pop() : {}); - - // Path arguments are determined after required parameters - const apiPathArgs = args.slice(0, apiPathParamsCount); - - const requestMethod = requestType?.toUpperCase() === 'POST' ? 'postRequest' : 'getRequest'; - - return this[requestMethod](buildApiPath(apiPathArgs), params, callback); - }.bind(this); - - this[methodName] = createFunction(methodName, namedParams, methodBody); - } - /** * Creates a post on the given blog. * diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 28259a01..6a951d51 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -16,8 +16,6 @@ const DUMMY_CREDENTIALS = { const DUMMY_API_URL = 'https://example.com'; -const URL_PARAM_REGEX = /\/:([^/]+)/g; - describe('tumblr.js', function () { /** @type {const} */ ([ ['createClient', (options) => tumblr.createClient(options)], @@ -407,114 +405,5 @@ describe('tumblr.js', function () { }); }); }); - - /** - * ## Request methods - * - * Test the methods that add methods to the client - * - * - TumblrClient#addGetMethods - */ - - /** @type {const} */ ([['get', 'addGetMethods']]).forEach(function ([ - httpMethod, - clientMethod, - ]) { - describe('#' + clientMethod, function () { - const client = new TumblrClient({ - ...DUMMY_CREDENTIALS, - baseUrl: DUMMY_API_URL, - }); - - const data = { - meta: { - status: 200, - msg: 'k', - }, - body: { - response: { - ayy: 'lmao', - }, - }, - }; - - const addMethods = - /** @type {Record]>} */ ({ - noPathParameters: ['/no/params', []], - onePathParameter: ['/one/:url/param', []], - twoPathParameters: ['/one/:url/param', []], - requiredParams: ['/query/params', ['id']], - pathAndRequiredParams: ['/query/:url/params', ['id']], - }); - - beforeEach(function () { - client[clientMethod](addMethods); - }); - - Object.entries(addMethods).forEach(function ([methodName, [apiPath, params]]) { - describe(methodName, function () { - let callbackInvoked, requestError, requestResponse; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - const queryParams = {}; - const args = []; - - apiPath.match(URL_PARAM_REGEX)?.forEach(function (apiPathParam) { - args.push(apiPathParam.replace(URL_PARAM_REGEX, '$1')); - }); - params.forEach(function (param) { - queryParams[param] = param + ' value'; - args.push(queryParams[param]); - }); - apiPath = apiPath.replace(URL_PARAM_REGEX, '/$1'); - - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - const scope = nock(client.baseUrl)[httpMethod](apiPath); - if (params.length) { - scope.query(true); - } - - scope.reply(data.meta.status, data.body).persist(); - - return client[methodName].apply( - client, - args.concat(function (...args) { - callback.call(client, ...args); - done(); - }), - ); - }); - - afterEach(function () { - nock.cleanAll(); - }); - - it('method is a function', function () { - assert.isFunction(client[methodName]); - }); - - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); - }); - }); - }); - }); - }); - - /** - * ~fin~ - */ }); }); From 6e41b1450e839a3f587d40ca3023a3f30bfe743f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 20:15:06 +0200 Subject: [PATCH 52/74] fixup! Remove addMethods --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd8ec03..7865309e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Removed +- **Breaking** The `addGetMethods` and `addPostMethods` methods have been removed. Additional + methods can be implemented using the `getRequest` or `postRequest` methods. - **Breaking** The following legacy post creation methods have been removed. - `createAudioPost`: use `ceatePost` with `{type: "audio"}`. - `createChatPost`: use `ceatePost` with `{type: "chat"}`. From 247e4277086537a4fb1071ffaf502ac0ff83b3a9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 20:42:37 +0200 Subject: [PATCH 53/74] fixup! Move deletePost into class --- lib/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index bf997194..5f07347a 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -504,7 +504,7 @@ class TumblrClient { * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ deletePost(blogIdentifier, params, callback) { - return this.postRequest('/v2/blog/:blogIdentifier/post/delete', params, callback); + return this.postRequest(`/v2/blog/${blogIdentifier}/post/delete`, params, callback); } /** From ce5173bde9193591f9d530ecbb4a6aec90db14ab Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:10:57 +0200 Subject: [PATCH 54/74] Remove returnPromises --- CHANGELOG.md | 2 ++ lib/tumblr.js | 45 ++++++++++++++++++++++++++++++++++++--------- test/tumblr.test.js | 18 +----------------- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7865309e..f7b781c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Removed +- **Breaking** API methods return promises when no callback is provided. The `returnPromises` method + and option have no effect. - **Breaking** The `addGetMethods` and `addPostMethods` methods have been removed. Additional methods can be implemented using the `getRequest` or `postRequest` methods. - **Breaking** The following legacy post creation methods have been removed. diff --git a/lib/tumblr.js b/lib/tumblr.js index 5f07347a..22a49193 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -218,7 +218,7 @@ class TumblrClient { ) : null; - // Enable Promise mode + // Deprecated, let it show its warning. if (options?.returnPromises) { this.returnPromises(); } @@ -251,16 +251,45 @@ class TumblrClient { } /** + * @template {TumblrClientCallback|undefined} CB + * * @param {URL} url * @param {'GET'|'POST'} method request method * @param {null|RequestData} data - * @param {TumblrClientCallback} [callback] + * @param {CB} providedCallback * - * @returns {Promise|undefined} + * @returns {CB extends undefined ? Promise : undefined} * * @private */ - makeRequest(url, method, data, callback) { + makeRequest(url, method, data, providedCallback) { + /** @type {TumblrClientCallback} */ + let callback; + + /** @type {Promise|undefined} */ + let promise; + if (!providedCallback) { + /** @type {(value: any) => void} */ + let resolve; + /** @type {(reason?: any) => void} */ + let reject; + + promise = new Promise((promiseResolve, promiseReject) => { + resolve = promiseResolve; + reject = promiseReject; + }); + + callback = (err, resp) => { + if (err) { + reject(err); + return; + } + resolve(resp); + }; + } else { + callback = providedCallback; + } + const httpModel = url.protocol === 'http' ? http : https; if (this.credentials.auth === 'apiKey') { @@ -370,7 +399,7 @@ class TumblrClient { }); request.end(); - return; + return /** @type {CB extends undefined ? Promise : undefined} */ (promise); } /** @@ -406,12 +435,10 @@ class TumblrClient { } /** - * Sets the client to return Promises instead of Request objects by patching the `getRequest` and - * `postRequest` methods on the client + * @deprecated Promises are returned if no callback is provided */ returnPromises() { - this.getRequest = promisifyRequest(this.getRequest); - this.postRequest = promisifyRequest(this.postRequest); + console.warn('returnPromises is deprecated. Promises are returned if no callback is provided.'); } /** diff --git a/test/tumblr.test.js b/test/tumblr.test.js index 6a951d51..f06edeef 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -102,17 +102,6 @@ describe('tumblr.js', function () { const tumblr = require('../lib/tumblr.js'); const TumblrClient = tumblr.Client; - describe('#returnPromises', function () { - it('modifies getRequest and postRequest', function () { - const client = new TumblrClient(DUMMY_CREDENTIALS); - const getRequestBefore = client.getRequest; - const postRequestBefore = client.postRequest; - client.returnPromises(); - assert.notEqual(getRequestBefore, client.getRequest); - assert.notEqual(postRequestBefore, client.postRequest); - }); - }); - /** * ## Default methods * @@ -167,7 +156,6 @@ describe('tumblr.js', function () { const client = new TumblrClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL, - returnPromises: true, }); const scope = nock(client.baseUrl, { reqheaders: { @@ -195,7 +183,7 @@ describe('tumblr.js', function () { }); it('get request sends api_key when all creds are not provided', async () => { - const client = new TumblrClient({ consumer_key: 'abc123', returnPromises: true }); + const client = new TumblrClient({ consumer_key: 'abc123' }); const scope = nock(client.baseUrl, { badheaders: ['authorization'], }) @@ -212,7 +200,6 @@ describe('tumblr.js', function () { const client = new TumblrClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL, - returnPromises: true, }); const scope = nock(client.baseUrl, { reqheaders: { @@ -248,7 +235,6 @@ describe('tumblr.js', function () { const client = new TumblrClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL, - returnPromises: true, }); const scope = nock(client.baseUrl, { badheaders: ['content-length', 'content-type'], @@ -279,7 +265,6 @@ describe('tumblr.js', function () { it('post request sends api_key when all creds are not provided', async () => { const client = new TumblrClient({ consumer_key: 'abc123' }); - client.returnPromises(); const scope = nock(client.baseUrl, { badheaders: ['authorization'], }) @@ -354,7 +339,6 @@ describe('tumblr.js', function () { const client = new TumblrClient({ ...DUMMY_CREDENTIALS, baseUrl: DUMMY_API_URL, - returnPromises: true, }); Object.entries(fixtures).forEach(function ([apiPath, data]) { From 88563988c9cb9fec0c6b3a9ef817a464253d3d55 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:12:12 +0200 Subject: [PATCH 55/74] Add more CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b781c7..8e08e93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `credentials` property. - **Breaking** The (optional) `baseUrl` option should be of the form `https://example.com` with no pathname, search, hash, etc. Bad `baseUrl` options will throw. +- Some API methods had documented signatures that were probably wrong. These have been updated. +- Bundled type declarations are now generated from source and should be improved. ### Deprecated @@ -26,6 +28,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `createPost` - `editPost` - `reblogPost` +- The callback API is considered deprecated in favor of the `Promise` API. ### Fixed @@ -48,6 +51,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `createVideoPost`: use `ceatePost` with `{type: "video"}`. - **Breaking** The `request` option has been removed. - The dependency on the deprecated `request` library has been removed. +- Request objects are no longer returned from API methods. ## [3.0.0] - 2020-07-28 From 9e475df101f6f009daf3883b689e3ddabb4ca20a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:12:33 +0200 Subject: [PATCH 56/74] fixup! Remove returnPromises --- lib/tumblr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 22a49193..78bd03c4 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -333,8 +333,8 @@ class TumblrClient { form.pipe(request); } - var responseData = ''; - var callbackCalled = false; + let responseData = ''; + let callbackCalled = false; request.on('response', function (response) { if (!callback) { From c15447ced9dfc663645b5ee947796561ac73083b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:19:12 +0200 Subject: [PATCH 57/74] Update promise related tests --- test/tumblr.test.js | 62 ++++++++++++--------------------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/test/tumblr.test.js b/test/tumblr.test.js index f06edeef..282e3abf 100644 --- a/test/tumblr.test.js +++ b/test/tumblr.test.js @@ -314,75 +314,45 @@ describe('tumblr.js', function () { describe(apiPath, function () { setupNockBeforeAfter(httpMethod, data, apiPath); - it('params and callback invokes callback with a successful response', function (done) { - const returnValue = client[clientMethod](apiPath, { foo: 'bar' }, (err, resp) => { + it('returns undefined when a callback is provided', (done) => { + const returnValue = client[clientMethod](apiPath, { foo: 'bar' }, () => { + done(); + }); + assert.isUndefined(returnValue); + }); + + it('callback is invoked with provided params', function (done) { + client[clientMethod](apiPath, { foo: 'bar' }, (err, resp) => { assert.isNull(err); assert.isDefined(resp); done(); }); - assert.isUndefined(returnValue); }); - it('callback only invokes callback with a successful response', function (done) { - const returnValue = client[clientMethod](apiPath, (err, resp) => { + it('callback is invoked without params', function (done) { + client[clientMethod](apiPath, (err, resp) => { assert.isNull(err); assert.isDefined(resp); done(); }); - assert.isUndefined(returnValue); }); }); }); }); describe('with promises', function () { - const client = new TumblrClient({ - ...DUMMY_CREDENTIALS, - baseUrl: DUMMY_API_URL, - }); - Object.entries(fixtures).forEach(function ([apiPath, data]) { describe(apiPath, function () { - let callbackInvoked, requestError, requestResponse, returnValue; - const params = {}; - const callback = function (err, resp) { - callbackInvoked = true; - requestError = err; - requestResponse = resp; - }; - setupNockBeforeAfter(httpMethod, data, apiPath); - beforeEach(function (done) { - callbackInvoked = false; - requestError = false; - requestResponse = false; - - returnValue = client[clientMethod](apiPath, params); - // Invoke the callback when the Promise resolves or rejects - returnValue.then( - function (resp) { - callback(null, resp); - done(); - }, - function (err) { - callback(err, null); - done(); - }, - ); - }); - - it('returns a Promise', function () { + it('returns a Promise', async () => { + const returnValue = client[clientMethod](apiPath, {}); assert.isTrue(returnValue instanceof Promise); + await returnValue; }); - it('invokes the callback', function () { - assert.isTrue(callbackInvoked); - }); - - it('gets a successful response', function () { - assert.isNull(requestError, 'err is falsy'); - assert.isDefined(requestResponse); + it('gets a successful response', async () => { + assert.isOk(await client[clientMethod](apiPath, {})); }); }); }); From 3dc8aa2ffb1cd3a54ea10b5217b1def68bd0d25b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:19:51 +0200 Subject: [PATCH 58/74] fixup! Remove returnPromises --- lib/tumblr.js | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 78bd03c4..a389d5ea 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -18,43 +18,6 @@ const oauth = require('oauth'); const CLIENT_VERSION = '4.0.0-alpha.0'; const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash -/** - * Take a callback-based function and returns a Promise instead - * - * @param {Function} requestMethod - callback-based method to promisify - * - * @return {Function} function that returns a Promise that resolves with the response body or - * rejects with the error message - * - * @private - */ -function promisifyRequest(requestMethod) { - /** @this {TumblrClient} */ - return function (apiPath, params, callback) { - const promise = new Promise((resolve, reject) => { - requestMethod.call(this, apiPath, params, function (err, resp) { - if (err) { - reject(err); - } else { - resolve(resp); - } - }); - }); - - if (callback) { - promise - .then(function (body) { - callback(null, body); - }) - .catch(function (err) { - callback(err, null); - }); - } - - return promise; - }; -} - /** * @typedef Options * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. From 0ae5ad7908b67f91567f9feac75443bb2ee8a323 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 21:21:59 +0200 Subject: [PATCH 59/74] fixup! fixup! Remove returnPromises --- integration/read-only.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/integration/read-only.mjs b/integration/read-only.mjs index ec895ca6..c3d4293c 100644 --- a/integration/read-only.mjs +++ b/integration/read-only.mjs @@ -13,7 +13,6 @@ describe('unauthorized requests', () => { let client; before(() => { client = new Client(); - client.returnPromises(); }); ['staff', 'staff.tumblr.com', 't:0aY0xL2Fi1OFJg4YxpmegQ'].forEach((blogIdentifier) => { @@ -35,7 +34,6 @@ describe('consumer_key (api_key) only requests', () => { client = new Client({ consumer_key: env.TUMBLR_OAUTH_CONSUMER_KEY, - returnPromises: true, }); }); @@ -73,7 +71,6 @@ describe('oauth1 requests', () => { token: env.TUMBLR_OAUTH_TOKEN, token_secret: env.TUMBLR_OAUTH_TOKEN_SECRET, }); - client.returnPromises(); }); test('fetches userInfo()', async () => { From 0ab961e700cf63f50d36ef00fbb4b71c1ee29214 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 23:06:59 +0200 Subject: [PATCH 60/74] Finish TypeScript implementation --- integration/read-only.mjs | 16 ++++++++-------- integration/write.mjs | 19 +++++++++---------- tsconfig.test.json | 4 +--- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/integration/read-only.mjs b/integration/read-only.mjs index c3d4293c..eeddf89f 100644 --- a/integration/read-only.mjs +++ b/integration/read-only.mjs @@ -9,7 +9,7 @@ beforeEach(function () { }); describe('unauthorized requests', () => { - /** @type {Client} */ + /** @type {import('tumblr.js').Client} */ let client; before(() => { client = new Client(); @@ -27,13 +27,13 @@ describe('consumer_key (api_key) only requests', () => { /** @type {import('tumblr.js').Client} */ let client; before(function () { - if (!env.TUMBLR_OAUTH_CONSUMER_KEY) { + if (!env['TUMBLR_OAUTH_CONSUMER_KEY']) { console.log('Provide TUMBLR_OAUTH_CONSUMER_KEY environment variable to run this block'); this.skip(); } client = new Client({ - consumer_key: env.TUMBLR_OAUTH_CONSUMER_KEY, + consumer_key: env['TUMBLR_OAUTH_CONSUMER_KEY'], }); }); @@ -55,7 +55,7 @@ describe('oauth1 requests', () => { 'TUMBLR_OAUTH_CONSUMER_SECRET', ]; - /** @type {Client} */ + /** @type {import('tumblr.js').Client} */ let client; before(function () { if (!OAUTH1_ENV_VARS.every((envVarName) => Boolean(env[envVarName]))) { @@ -66,10 +66,10 @@ describe('oauth1 requests', () => { } client = new Client({ - consumer_key: env.TUMBLR_OAUTH_CONSUMER_KEY, - consumer_secret: env.TUMBLR_OAUTH_CONSUMER_SECRET, - token: env.TUMBLR_OAUTH_TOKEN, - token_secret: env.TUMBLR_OAUTH_TOKEN_SECRET, + consumer_key: /** @type {string} */ (env['TUMBLR_OAUTH_CONSUMER_KEY']), + consumer_secret: /** @type {string} */ (env['TUMBLR_OAUTH_CONSUMER_SECRET']), + token: /** @type {string} */ (env['TUMBLR_OAUTH_TOKEN']), + token_secret: /** @type {string} */ (env['TUMBLR_OAUTH_TOKEN_SECRET']), }); }); diff --git a/integration/write.mjs b/integration/write.mjs index bc65c544..d977b8fd 100644 --- a/integration/write.mjs +++ b/integration/write.mjs @@ -14,16 +14,16 @@ describe('oauth1 write requests', () => { before(async function () { if ( - !env.TUMBLR_OAUTH_CONSUMER_KEY || - !env.TUMBLR_OAUTH_CONSUMER_SECRET || - !env.TUMBLR_OAUTH_TOKEN || - !env.TUMBLR_OAUTH_TOKEN_SECRET + !env['TUMBLR_OAUTH_CONSUMER_KEY'] || + !env['TUMBLR_OAUTH_CONSUMER_SECRET'] || + !env['TUMBLR_OAUTH_TOKEN'] || + !env['TUMBLR_OAUTH_TOKEN_SECRET'] ) { console.log('Must provide all Oauth1 environment variables'); this.skip(); } - if (!env.CI) { + if (!env['CI']) { console.warn( 'This test suite uses the API to make changes. Modify the test suite to enabled it.', ); @@ -31,11 +31,10 @@ describe('oauth1 write requests', () => { } client = new Client({ - consumer_key: env.TUMBLR_OAUTH_CONSUMER_KEY, - consumer_secret: env.TUMBLR_OAUTH_CONSUMER_SECRET, - token: env.TUMBLR_OAUTH_TOKEN, - token_secret: env.TUMBLR_OAUTH_TOKEN_SECRET, - returnPromises: true, + consumer_key: env['TUMBLR_OAUTH_CONSUMER_KEY'], + consumer_secret: env['TUMBLR_OAUTH_CONSUMER_SECRET'], + token: env['TUMBLR_OAUTH_TOKEN'], + token_secret: env['TUMBLR_OAUTH_TOKEN_SECRET'], }); const userResp = await client.userInfo(); diff --git a/tsconfig.test.json b/tsconfig.test.json index 7e23586c..2ea1311f 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,9 +1,7 @@ { "extends": ["@tsconfig/strictest/tsconfig.json", "@tsconfig/node16/tsconfig.json"], "compilerOptions": { - "composite": true, - "declaration": true, - "emitDeclarationOnly": true, + "noEmit": true, "rootDir": ".", "moduleResolution": "node16", "types": ["node", "mocha"], From c843b025058b65a9fec91a211a393e6bc7a9ec81 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 4 Aug 2023 23:07:40 +0200 Subject: [PATCH 61/74] Compile TS declaration file --- lib/tumblr.d.ts | 592 +++++++++++++++++++++++++++++++----------------- 1 file changed, 387 insertions(+), 205 deletions(-) diff --git a/lib/tumblr.d.ts b/lib/tumblr.d.ts index c4d935f8..df7afe5d 100644 --- a/lib/tumblr.d.ts +++ b/lib/tumblr.d.ts @@ -1,212 +1,394 @@ -declare module 'tumblr.js' { - type TumblrClientCallback = (err: any, resp: any, rawResp?: string) => void; - - interface Options { - /** OAuth1 credential. Required for API key auth endpoints. */ +export type Options = { + /** + * OAuth1 credential. Required for API key auth endpoints. + */ consumer_key?: string; - /** OAuth1 credential. Required for OAuth endpoints. */ + /** + * OAuth1 credential. Required for OAuth endpoints. + */ consumer_secret?: string; - /** OAuth1 credential. Required for OAuth endpoints. */ + /** + * OAuth1 credential. Required for OAuth endpoints. + */ token?: string; - /** OAuth1 credential. Required for Oauth endpoints. */ + /** + * OAuth1 credential. Required for Oauth endpoints. + */ token_secret?: string; - /** (optional) The API url if different from the default. */ + /** + * (optional) The API url if different from the default. + */ baseUrl?: string; - /** (optional) Use promises instead of callbacks. */ + /** + * (optional) Use promises instead of callbacks. + */ returnPromises?: boolean; - } - - class TumblrClient { - constructor(options?: Options); - - userInfo(callback: TumblrClientCallback): void; - - blogAvatar( - blogIdentifier: string, - size: number, - params: object, - callback: TumblrClientCallback, - ): void; - blogAvatar(blogIdentifier: string, size: number, callback: TumblrClientCallback): void; - blogAvatar(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogAvatar(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogDrafts(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogDrafts(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogFollowers(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogFollowers(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogInfo(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogInfo(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogLikes(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogLikes(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogPosts(blogIdentifier: string): void; - blogPosts(blogIdentifier: string, type: string): void; - blogPosts(blogIdentifier: string, type: string, params: any): void; - blogPosts(blogIdentifier: string, params: any, callback: TumblrClientCallback): void; - blogPosts(blogIdentifier: string, callback: TumblrClientCallback): void; - blogPosts( - blogIdentifier: string, - type: string, - params: any, - callback: TumblrClientCallback, - ): void; - - blogSubmissions(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogSubmissions(blogIdentifier: string, callback: TumblrClientCallback): void; - - blogQueue(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - blogQueue(blogIdentifier: string, callback: TumblrClientCallback): void; - - createTextPost( - blogIdentifier: string, - options: TextPostParams, - callback: TumblrClientCallback, - ): void; - createPhotoPost( - blogIdentifier: string, - options: PhotoPostParams, - callback: TumblrClientCallback, - ): void; - createQuotePost( - blogIdentifier: string, - options: QuotePostParams, - callback: TumblrClientCallback, - ): void; - createLinkPost( - blogIdentifier: string, - options: LinkPostParams, - callback: TumblrClientCallback, - ): void; - createChatPost( - blogIdentifier: string, - options: ChatPostParams, - callback: TumblrClientCallback, - ): void; - createAudioPost( - blogIdentifier: string, - options: AudioPostParams, - callback: TumblrClientCallback, - ): void; - createVideoPost( - blogIdentifier: string, - options: VideoPostParams, - callback: TumblrClientCallback, - ): void; - - taggedPosts(tag: string, options: object, callback: TumblrClientCallback): void; - taggedPosts(tag: string, callback: TumblrClientCallback): void; - - deletePost(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - deletePost(blogIdentifier: string, id: number | string, callback: TumblrClientCallback): void; - - editPost(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - - reblogPost(blogIdentifier: string, params: object, callback: TumblrClientCallback): void; - - userDashboard(params: object, callback: TumblrClientCallback): void; - userDashboard(callback: TumblrClientCallback): void; - - userLikes(params: object, callback: TumblrClientCallback): void; - userLikes(callback: TumblrClientCallback): void; - - userFollowing(params: object, callback: TumblrClientCallback): void; - userFollowing(callback: TumblrClientCallback): void; - - followBlog(params: object, callback: TumblrClientCallback): void; - followBlog(blogURL: string, callback: TumblrClientCallback): void; - - unfollowBlog(params: object, callback: TumblrClientCallback): void; - unfollowBlog(blogURL: string, callback: TumblrClientCallback): void; - - likePost(params: object, callback: TumblrClientCallback): void; - likePost(id: number | string, reblogKey: string, callback: TumblrClientCallback): void; - - unlikePost(params: object, callback: TumblrClientCallback): void; - unlikePost(id: number | string, reblogKey: string, callback: TumblrClientCallback): void; - - getRequest( - apiPath: string, - paramsOrCallback?: object | null | TumblrClientCallback, - callback?: TumblrClientCallback, - ): Request; - postRequest( - apiPath: string, - paramsOrCallback?: object | null | TumblrClientCallback, - callback?: TumblrClientCallback, - ): Request; - } - - function createClient(options?: Options): TumblrClient; - - interface TextPostParams { - title?: string; - body: string; - } - - interface PhotoPostParamsWithSource { - source: string; - caption?: string; - } - - interface PhotoPostParamsWithData { - data: any | Array; - caption?: string; - } - - interface PhotoPostParamsWithData64 { - data64: string; - caption?: string; - } - - type PhotoPostParams = - | PhotoPostParamsWithSource - | PhotoPostParamsWithData - | PhotoPostParamsWithData64; - - interface QuotePostParams { - quote: string; - source?: string; - } - - interface LinkPostParams { - title?: string; - url: string; - thumbnail?: string; - excerpt?: string; - author?: string; - description?: string; - } - - interface ChatPostParams { - title?: string; - conversation: string; - } - - interface AudioPostParamsWithExternalUrl { - external_url: string; - caption?: string; - } - - interface AudioPostParamsWithData { - data: any; - caption?: string; - } - - type AudioPostParams = AudioPostParamsWithExternalUrl | AudioPostParamsWithData; - - interface VideoPostParamsWithEmbed { - embed: string; - caption?: string; - } - - interface VideoPostParamsWithData { - data: any; - caption?: string; - } - - type VideoPostParams = VideoPostParamsWithEmbed | VideoPostParamsWithData; +}; +/** + * Handles the response from a client reuest + */ +export type TumblrClientCallback = (err: Error | null, resp: any | null, response?: http.IncomingMessage | null | undefined) => void; +/** + * @typedef Options + * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. + * @property {string} [consumer_secret] OAuth1 credential. Required for OAuth endpoints. + * @property {string} [token] OAuth1 credential. Required for OAuth endpoints. + * @property {string} [token_secret] OAuth1 credential. Required for Oauth endpoints. + * @property {string} [baseUrl] (optional) The API url if different from the default. + * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. + */ +/** + * Handles the response from a client reuest + * + * @callback TumblrClientCallback + * @param {?Error} err - error message + * @param {?Object} resp - response body + * @param {?http.IncomingMessage} [response] - raw response + * @returns {void} + */ +declare class TumblrClient { + /** + * @typedef {Map|string>} RequestData + * + * @typedef {'text'|'quote'|'link'|'answer'|'video'|'audio'|'photo'|'chat'} PostType + * + * @typedef {'text'|'raw'} PostFormatFilter + * + * + * @typedef {{readonly auth:'none'}} NoneAuthCredentials + * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials + * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials + * @typedef {NoneAuthCredentials|ApiKeyCredentials|OAuth1Credentials} Credentials + */ + /** + * Creates a Tumblr API client using the given options + * + * @param {Options} [options] - client options + * + * @constructor + */ + constructor(options?: Options | undefined); + /** + * Package version + * @type {typeof CLIENT_VERSION} + */ + version: typeof CLIENT_VERSION; + /** + * Base URL to API requests + * @type {string} + */ + baseUrl: string; + /** @type {Credentials} */ + credentials: { + readonly auth: 'none'; + } | { + readonly auth: 'apiKey'; + readonly apiKey: string; + } | { + readonly auth: 'oauth1'; + readonly consumer_key: string; + readonly consumer_secret: string; + readonly token: string; + readonly token_secret: string; + }; + /** @type {oauth.OAuth | null} */ + oauthClient: oauth.OAuth | null; + /** + * Performs a GET request + * + * @param {string} apiPath - URL path for the request + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - request callback + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + getRequest(apiPath: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * @template {TumblrClientCallback|undefined} CB + * + * @param {URL} url + * @param {'GET'|'POST'} method request method + * @param {null|RequestData} data + * @param {CB} providedCallback + * + * @returns {CB extends undefined ? Promise : undefined} + * + * @private + */ + private makeRequest; + /** + * Performs a POST request + * + * @param {string} apiPath - URL path for the request + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + postRequest(apiPath: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * @deprecated Promises are returned if no callback is provided + */ + returnPromises(): void; + /** + * Creates a post on the given blog. + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + createPost(blogIdentifier: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Edits a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + editPost(blogIdentifier: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Likes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + likePost(params: { + id: string; + reblog_key: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Unlikes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unlikePost(params: { + id: string; + reblog_key: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Follows a blog as the authenticating user + * + * @param {{url: string}|{email:string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + followBlog(params: { + url: string; + } | { + email: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Unfollows a blog as the authenticating user + * + * @param {{url: string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unfollowBlog(params: { + url: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Deletes a given post + * + * @param {string} blogIdentifier - blog name or URL + * @param {{id:string}} params + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + deletePost(blogIdentifier: string, params: { + id: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Reblogs a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + reblogPost(blogIdentifier: string, params: Record, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets information about a given blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogInfo(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + 'fields[blogs]'?: string; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the likes for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogLikes(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + limit?: number; + offset?: number; + before?: number; + after?: number; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the followers for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogFollowers(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + limit?: number; + offset?: number; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets a list of posts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + */ + blogPosts(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + type?: "text" | "quote" | "link" | "answer" | "video" | "audio" | "photo" | "chat"; + limit?: number; + offset?: number; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the queue for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogQueue(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + limit?: number; + offset?: number; + filter?: 'text' | 'raw'; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the drafts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogDrafts(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + before_id?: number; + filter?: "text" | "raw"; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the submissions for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogSubmissions(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + offset?: number; + filter?: "text" | "raw"; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the avatar URL for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{size?: 16|24|30|40|48|64|96|128|512}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [maybeCallback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogAvatar(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { + size?: 16 | 24 | 30 | 40 | 48 | 64 | 96 | 128 | 512; + } | undefined, maybeCallback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets information about the authenticating user and their blogs + * + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userInfo(callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the dashboard posts for the authenticating user + * + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userDashboard(paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the blogs the authenticating user follows + * + * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userFollowing(paramsOrCallback?: TumblrClientCallback | { + limit?: number; + offset?: number; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the likes for the authenticating user + * + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userLikes(paramsOrCallback?: TumblrClientCallback | { + limit?: number; + offset?: number; + before?: number; + after?: number; + } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets posts tagged with the specified tag + * + * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + taggedPosts(params: { + [param: string]: string | number; + tag: string; + }, callback?: TumblrClientCallback | undefined): Promise | undefined; } +declare const CLIENT_VERSION: "4.0.0-alpha.0"; +import oauth = require("oauth"); +export declare function createClient(options?: Options | undefined): TumblrClient; +export { TumblrClient as Client }; From 1339712778494fa19a7e8e3fed7c96ae95b1abe3 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Aug 2023 20:05:58 +0200 Subject: [PATCH 62/74] Add eslint-disable for expected console.warn --- lib/tumblr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tumblr.js b/lib/tumblr.js index a389d5ea..0ae66c69 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -401,6 +401,7 @@ class TumblrClient { * @deprecated Promises are returned if no callback is provided */ returnPromises() { + // eslint-disable-next-line no-console console.warn('returnPromises is deprecated. Promises are returned if no callback is provided.'); } From 4ba6fe2d9d59d4af39deb0479dc1f8add7e84f71 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Aug 2023 20:21:34 +0200 Subject: [PATCH 63/74] Autoformat tumblr.d.ts --- lib/tumblr.d.ts | 837 +++++++++++++++++++++++++++--------------------- 1 file changed, 478 insertions(+), 359 deletions(-) diff --git a/lib/tumblr.d.ts b/lib/tumblr.d.ts index df7afe5d..dcece478 100644 --- a/lib/tumblr.d.ts +++ b/lib/tumblr.d.ts @@ -1,33 +1,37 @@ export type Options = { - /** - * OAuth1 credential. Required for API key auth endpoints. - */ - consumer_key?: string; - /** - * OAuth1 credential. Required for OAuth endpoints. - */ - consumer_secret?: string; - /** - * OAuth1 credential. Required for OAuth endpoints. - */ - token?: string; - /** - * OAuth1 credential. Required for Oauth endpoints. - */ - token_secret?: string; - /** - * (optional) The API url if different from the default. - */ - baseUrl?: string; - /** - * (optional) Use promises instead of callbacks. - */ - returnPromises?: boolean; + /** + * OAuth1 credential. Required for API key auth endpoints. + */ + consumer_key?: string; + /** + * OAuth1 credential. Required for OAuth endpoints. + */ + consumer_secret?: string; + /** + * OAuth1 credential. Required for OAuth endpoints. + */ + token?: string; + /** + * OAuth1 credential. Required for Oauth endpoints. + */ + token_secret?: string; + /** + * (optional) The API url if different from the default. + */ + baseUrl?: string; + /** + * (optional) Use promises instead of callbacks. + */ + returnPromises?: boolean; }; /** * Handles the response from a client reuest */ -export type TumblrClientCallback = (err: Error | null, resp: any | null, response?: http.IncomingMessage | null | undefined) => void; +export type TumblrClientCallback = ( + err: Error | null, + resp: any | null, + response?: http.IncomingMessage | null | undefined, +) => void; /** * @typedef Options * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. @@ -47,348 +51,463 @@ export type TumblrClientCallback = (err: Error | null, resp: any | null, respons * @returns {void} */ declare class TumblrClient { - /** - * @typedef {Map|string>} RequestData - * - * @typedef {'text'|'quote'|'link'|'answer'|'video'|'audio'|'photo'|'chat'} PostType - * - * @typedef {'text'|'raw'} PostFormatFilter - * - * - * @typedef {{readonly auth:'none'}} NoneAuthCredentials - * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials - * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials - * @typedef {NoneAuthCredentials|ApiKeyCredentials|OAuth1Credentials} Credentials - */ - /** - * Creates a Tumblr API client using the given options - * - * @param {Options} [options] - client options - * - * @constructor - */ - constructor(options?: Options | undefined); - /** - * Package version - * @type {typeof CLIENT_VERSION} - */ - version: typeof CLIENT_VERSION; - /** - * Base URL to API requests - * @type {string} - */ - baseUrl: string; - /** @type {Credentials} */ - credentials: { + /** + * @typedef {Map|string>} RequestData + * + * @typedef {'text'|'quote'|'link'|'answer'|'video'|'audio'|'photo'|'chat'} PostType + * + * @typedef {'text'|'raw'} PostFormatFilter + * + * + * @typedef {{readonly auth:'none'}} NoneAuthCredentials + * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials + * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials + * @typedef {NoneAuthCredentials|ApiKeyCredentials|OAuth1Credentials} Credentials + */ + /** + * Creates a Tumblr API client using the given options + * + * @param {Options} [options] - client options + * + * @constructor + */ + constructor(options?: Options | undefined); + /** + * Package version + * @type {typeof CLIENT_VERSION} + */ + version: typeof CLIENT_VERSION; + /** + * Base URL to API requests + * @type {string} + */ + baseUrl: string; + /** @type {Credentials} */ + credentials: + | { readonly auth: 'none'; - } | { + } + | { readonly auth: 'apiKey'; readonly apiKey: string; - } | { + } + | { readonly auth: 'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string; - }; - /** @type {oauth.OAuth | null} */ - oauthClient: oauth.OAuth | null; - /** - * Performs a GET request - * - * @param {string} apiPath - URL path for the request - * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - request callback - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - getRequest(apiPath: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * @template {TumblrClientCallback|undefined} CB - * - * @param {URL} url - * @param {'GET'|'POST'} method request method - * @param {null|RequestData} data - * @param {CB} providedCallback - * - * @returns {CB extends undefined ? Promise : undefined} - * - * @private - */ - private makeRequest; - /** - * Performs a POST request - * - * @param {string} apiPath - URL path for the request - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - postRequest(apiPath: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * @deprecated Promises are returned if no callback is provided - */ - returnPromises(): void; - /** - * Creates a post on the given blog. - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - createPost(blogIdentifier: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Edits a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - editPost(blogIdentifier: string, paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Likes a post as the authenticating user - * - * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - likePost(params: { - id: string; - reblog_key: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Unlikes a post as the authenticating user - * - * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unlikePost(params: { - id: string; - reblog_key: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Follows a blog as the authenticating user - * - * @param {{url: string}|{email:string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - followBlog(params: { - url: string; - } | { - email: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Unfollows a blog as the authenticating user - * - * @param {{url: string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unfollowBlog(params: { - url: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Deletes a given post - * - * @param {string} blogIdentifier - blog name or URL - * @param {{id:string}} params - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - deletePost(blogIdentifier: string, params: { - id: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Reblogs a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - reblogPost(blogIdentifier: string, params: Record, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets information about a given blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogInfo(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - 'fields[blogs]'?: string; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the likes for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogLikes(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - limit?: number; - offset?: number; - before?: number; - after?: number; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the followers for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogFollowers(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - limit?: number; - offset?: number; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets a list of posts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - */ - blogPosts(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - type?: "text" | "quote" | "link" | "answer" | "video" | "audio" | "photo" | "chat"; - limit?: number; - offset?: number; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the queue for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogQueue(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - limit?: number; - offset?: number; - filter?: 'text' | 'raw'; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the drafts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogDrafts(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - before_id?: number; - filter?: "text" | "raw"; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the submissions for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogSubmissions(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - offset?: number; - filter?: "text" | "raw"; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the avatar URL for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{size?: 16|24|30|40|48|64|96|128|512}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [maybeCallback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogAvatar(blogIdentifier: string, paramsOrCallback?: TumblrClientCallback | { - size?: 16 | 24 | 30 | 40 | 48 | 64 | 96 | 128 | 512; - } | undefined, maybeCallback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets information about the authenticating user and their blogs - * - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userInfo(callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the dashboard posts for the authenticating user - * - * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userDashboard(paramsOrCallback?: Record | TumblrClientCallback | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the blogs the authenticating user follows - * - * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userFollowing(paramsOrCallback?: TumblrClientCallback | { - limit?: number; - offset?: number; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the likes for the authenticating user - * - * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userLikes(paramsOrCallback?: TumblrClientCallback | { - limit?: number; - offset?: number; - before?: number; - after?: number; - } | undefined, callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets posts tagged with the specified tag - * - * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - taggedPosts(params: { - [param: string]: string | number; - tag: string; - }, callback?: TumblrClientCallback | undefined): Promise | undefined; + }; + /** @type {oauth.OAuth | null} */ + oauthClient: oauth.OAuth | null; + /** + * Performs a GET request + * + * @param {string} apiPath - URL path for the request + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - request callback + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + getRequest( + apiPath: string, + paramsOrCallback?: Record | TumblrClientCallback | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * @template {TumblrClientCallback|undefined} CB + * + * @param {URL} url + * @param {'GET'|'POST'} method request method + * @param {null|RequestData} data + * @param {CB} providedCallback + * + * @returns {CB extends undefined ? Promise : undefined} + * + * @private + */ + private makeRequest; + /** + * Performs a POST request + * + * @param {string} apiPath - URL path for the request + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + postRequest( + apiPath: string, + paramsOrCallback?: Record | TumblrClientCallback | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * @deprecated Promises are returned if no callback is provided + */ + returnPromises(): void; + /** + * Creates a post on the given blog. + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + createPost( + blogIdentifier: string, + paramsOrCallback?: Record | TumblrClientCallback | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Edits a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record|TumblrClientCallback} [paramsOrCallback] + * @param {TumblrClientCallback} [callback] + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + editPost( + blogIdentifier: string, + paramsOrCallback?: Record | TumblrClientCallback | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Likes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + likePost( + params: { + id: string; + reblog_key: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Unlikes a post as the authenticating user + * + * @param {{ id: string; reblog_key: string }} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unlikePost( + params: { + id: string; + reblog_key: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Follows a blog as the authenticating user + * + * @param {{url: string}|{email:string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + followBlog( + params: + | { + url: string; + } + | { + email: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Unfollows a blog as the authenticating user + * + * @param {{url: string}} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + unfollowBlog( + params: { + url: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Deletes a given post + * + * @param {string} blogIdentifier - blog name or URL + * @param {{id:string}} params + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + deletePost( + blogIdentifier: string, + params: { + id: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Reblogs a given post + * + * @deprecated Legacy post creation methods are deprecated. Use NPF methods. + * + * @param {string} blogIdentifier - blog name or URL + * @param {Record} params - parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + reblogPost( + blogIdentifier: string, + params: Record, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets information about a given blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogInfo( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + 'fields[blogs]'?: string; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the likes for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogLikes( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + limit?: number; + offset?: number; + before?: number; + after?: number; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the followers for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogFollowers( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + limit?: number; + offset?: number; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets a list of posts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + */ + blogPosts( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + type?: 'text' | 'quote' | 'link' | 'answer' | 'video' | 'audio' | 'photo' | 'chat'; + limit?: number; + offset?: number; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the queue for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogQueue( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + limit?: number; + offset?: number; + filter?: 'text' | 'raw'; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the drafts for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogDrafts( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + before_id?: number; + filter?: 'text' | 'raw'; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the submissions for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogSubmissions( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + offset?: number; + filter?: 'text' | 'raw'; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the avatar URL for a blog + * + * @param {string} blogIdentifier - blog name or URL + * @param {{size?: 16|24|30|40|48|64|96|128|512}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request + * @param {TumblrClientCallback} [maybeCallback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + blogAvatar( + blogIdentifier: string, + paramsOrCallback?: + | TumblrClientCallback + | { + size?: 16 | 24 | 30 | 40 | 48 | 64 | 96 | 128 | 512; + } + | undefined, + maybeCallback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets information about the authenticating user and their blogs + * + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userInfo(callback?: TumblrClientCallback | undefined): Promise | undefined; + /** + * Gets the dashboard posts for the authenticating user + * + * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userDashboard( + paramsOrCallback?: Record | TumblrClientCallback | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the blogs the authenticating user follows + * + * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userFollowing( + paramsOrCallback?: + | TumblrClientCallback + | { + limit?: number; + offset?: number; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets the likes for the authenticating user + * + * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + userLikes( + paramsOrCallback?: + | TumblrClientCallback + | { + limit?: number; + offset?: number; + before?: number; + after?: number; + } + | undefined, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; + /** + * Gets posts tagged with the specified tag + * + * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request + * @param {TumblrClientCallback} [callback] - invoked when the request completes + * + * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + */ + taggedPosts( + params: { + [param: string]: string | number; + tag: string; + }, + callback?: TumblrClientCallback | undefined, + ): Promise | undefined; } -declare const CLIENT_VERSION: "4.0.0-alpha.0"; -import oauth = require("oauth"); +declare const CLIENT_VERSION: '4.0.0-alpha.0'; +import oauth = require('oauth'); export declare function createClient(options?: Options | undefined): TumblrClient; export { TumblrClient as Client }; From dca08aacebda4f17a8a3dc5ee9ac6a8adeb4a61c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 10:28:22 +0200 Subject: [PATCH 64/74] Run integration on PRs on approval --- .github/workflows/integration.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 6c0d8a5f..cef65554 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -1,7 +1,7 @@ name: Integration tests on: - workflow_dispatch: + pull_request: push: branches: - main @@ -11,6 +11,7 @@ jobs: tests: name: Integration testing with Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest + environment: integration-suite strategy: fail-fast: true From 0afcdb3662121961955c305d2a0c1da3faad5c1a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 10:30:15 +0200 Subject: [PATCH 65/74] Run integration on node lts only --- .github/workflows/integration.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index cef65554..3a69ceb3 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -18,9 +18,7 @@ jobs: max-parallel: 1 matrix: node-version: - - current - lts/* - - lts/-1 steps: - name: Checkout code From 925faf0000db515ff41e1feac96f90fc6847acca Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 10:45:26 +0200 Subject: [PATCH 66/74] Combine tests into single workflow --- .github/workflows/ci.yaml | 77 ++++++++++++++++++++++++++++++ .github/workflows/integration.yaml | 2 +- integration/write.mjs | 8 ++-- package.json | 3 +- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed8aaf85..b6e22981 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -67,3 +67,80 @@ jobs: - name: Run tests run: | npm run test:coverage + + integration: + name: Integration testing with Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: true + max-parallel: 1 + matrix: + node-version: + - lts/* + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: | + npm ci + + - name: Build + run: | + npm run build --if-present + + - name: Run integration tests + env: + TUMBLR_OAUTH_CONSUMER_KEY: ${{ secrets.TUMBLR_OAUTH_CONSUMER_KEY }} + TUMBLR_OAUTH_CONSUMER_SECRET: ${{ secrets.TUMBLR_OAUTH_CONSUMER_SECRET }} + TUMBLR_OAUTH_TOKEN: ${{ secrets.TUMBLR_OAUTH_TOKEN }} + TUMBLR_OAUTH_TOKEN_SECRET: ${{ secrets.TUMBLR_OAUTH_TOKEN_SECRET }} + run: | + npm run test:integration + + integration-write: + name: Integration testing with Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + environment: integration-suite + + strategy: + fail-fast: true + max-parallel: 1 + matrix: + node-version: + - lts/* + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: | + npm ci + + - name: Build + run: | + npm run build --if-present + + - name: Run integration tests + env: + TUMBLR_OAUTH_CONSUMER_KEY: ${{ secrets.TUMBLR_OAUTH_CONSUMER_KEY }} + TUMBLR_OAUTH_CONSUMER_SECRET: ${{ secrets.TUMBLR_OAUTH_CONSUMER_SECRET }} + TUMBLR_OAUTH_TOKEN: ${{ secrets.TUMBLR_OAUTH_TOKEN }} + TUMBLR_OAUTH_TOKEN_SECRET: ${{ secrets.TUMBLR_OAUTH_TOKEN_SECRET }} + run: | + npm run test:integration-write diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 3a69ceb3..c8eec847 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -8,7 +8,7 @@ on: - master jobs: - tests: + integration-read-only: name: Integration testing with Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest environment: integration-suite diff --git a/integration/write.mjs b/integration/write.mjs index d977b8fd..8d7cd262 100644 --- a/integration/write.mjs +++ b/integration/write.mjs @@ -58,8 +58,8 @@ describe('oauth1 write requests', () => { ); }); - describe('photo post', () => { - it('creates a post via data', async () => { + describe('create photo post', () => { + it('via data', async () => { const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url)); const res = await client.createPost(blogName, { @@ -72,7 +72,7 @@ describe('oauth1 write requests', () => { assert.isOk(res); }); - it('creates a slideshow post via data[]', async () => { + it('via data[]', async () => { const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url)); const res = await client.createPost(blogName, { @@ -85,7 +85,7 @@ describe('oauth1 write requests', () => { assert.isOk(res); }); - it(`creates a post via data64`, async () => { + it('via data64', async () => { const data = await readFile(new URL('../test/fixtures/image.jpg', import.meta.url), { encoding: 'base64', }); diff --git a/package.json b/package.json index e733c8be..858a19bd 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "gh-pages": "git subtree push --prefix=gh-pages origin gh-pages", "test": "mocha --timeout 100", "test:coverage": "nyc npm run test", - "test:integration": "mocha ./integration", + "test:integration": "mocha ./integration/read-only", + "test:integration-write": "mocha ./integration/write", "lint": "eslint --report-unused-disable-directives --ext 'js' --ext 'mjs' lib test integration" }, "engines": { From bb841a0ffbe666809b4be245ad7a4e67441bc2fe Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 10:47:52 +0200 Subject: [PATCH 67/74] fixup! Combine tests into single workflow --- .github/workflows/integration.yaml | 48 ------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 .github/workflows/integration.yaml diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml deleted file mode 100644 index c8eec847..00000000 --- a/.github/workflows/integration.yaml +++ /dev/null @@ -1,48 +0,0 @@ -name: Integration tests - -on: - pull_request: - push: - branches: - - main - - master - -jobs: - integration-read-only: - name: Integration testing with Node.js ${{ matrix.node-version }} - runs-on: ubuntu-latest - environment: integration-suite - - strategy: - fail-fast: true - max-parallel: 1 - matrix: - node-version: - - lts/* - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - - name: Install dependencies - run: | - npm ci - - - name: Build - run: | - npm run build --if-present - - - name: Run integration tests - env: - TUMBLR_OAUTH_CONSUMER_KEY: ${{ secrets.TUMBLR_OAUTH_CONSUMER_KEY }} - TUMBLR_OAUTH_CONSUMER_SECRET: ${{ secrets.TUMBLR_OAUTH_CONSUMER_SECRET }} - TUMBLR_OAUTH_TOKEN: ${{ secrets.TUMBLR_OAUTH_TOKEN }} - TUMBLR_OAUTH_TOKEN_SECRET: ${{ secrets.TUMBLR_OAUTH_TOKEN_SECRET }} - run: | - npm run test:integration From 048e7ed1b716fd63f76f99ac9a57b3d312ee70b1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 10:48:54 +0200 Subject: [PATCH 68/74] fixup! fixup! Combine tests into single workflow --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6e22981..4e6fd460 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,7 +69,7 @@ jobs: npm run test:coverage integration: - name: Integration testing with Node.js ${{ matrix.node-version }} + name: Integration API read-only tests runs-on: ubuntu-latest strategy: @@ -107,7 +107,7 @@ jobs: npm run test:integration integration-write: - name: Integration testing with Node.js ${{ matrix.node-version }} + name: Integration API write tests runs-on: ubuntu-latest environment: integration-suite From 8b9f216c48bf10ec4dc7955be4f541f293f009a9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 11:04:51 +0200 Subject: [PATCH 69/74] Remove lodash dependency --- package-lock.json | 10 ++-------- package.json | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e38e5a8..7d449879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,9 @@ "version": "4.0.0-alpha.0", "license": "Apache-2.0", "dependencies": { - "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", "form-data": "^4.0.0", - "lodash": "^4.17.11", "oauth": "^0.10.0" }, "devDependencies": { @@ -681,11 +679,6 @@ "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.14.196", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", - "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==" - }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", @@ -2348,7 +2341,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.flattendeep": { "version": "4.4.0", diff --git a/package.json b/package.json index 858a19bd..833eb833 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,9 @@ } ], "dependencies": { - "@types/lodash": "^4.0.0", "@types/node": ">=16", "@types/oauth": "^0.9.1", "form-data": "^4.0.0", - "lodash": "^4.17.11", "oauth": "^0.10.0" }, "devDependencies": { From ab95889d60185da023f0e6e7cfac8f95b9317951 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 11:18:58 +0200 Subject: [PATCH 70/74] Add deprecated note to returnPromises option --- lib/tumblr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 0ae66c69..5098bd1d 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -25,7 +25,7 @@ const API_BASE_URL = 'https://api.tumblr.com'; // deliberately no trailing slash * @property {string} [token] OAuth1 credential. Required for OAuth endpoints. * @property {string} [token_secret] OAuth1 credential. Required for Oauth endpoints. * @property {string} [baseUrl] (optional) The API url if different from the default. - * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. + * @property {boolean} [returnPromises] **Deprecated** Methods will return promises if no callback is provided. */ /** From 1429f8008018646c2ccbd2c4fd075bba27b59a14 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 12:19:46 +0200 Subject: [PATCH 71/74] CHANGELOG tweaks --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e08e93f..3a978566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added -- Integration test suites using real API. +- Integration test suites using the Tumblr API. ### Changed @@ -21,6 +21,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). pathname, search, hash, etc. Bad `baseUrl` options will throw. - Some API methods had documented signatures that were probably wrong. These have been updated. - Bundled type declarations are now generated from source and should be improved. +- Dependencies have changed, notably `request` (deprecated) and `lodash` have been removed. ### Deprecated @@ -50,7 +51,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `createTextPost`: use `ceatePost` with `{type: "text"}`. - `createVideoPost`: use `ceatePost` with `{type: "video"}`. - **Breaking** The `request` option has been removed. -- The dependency on the deprecated `request` library has been removed. - Request objects are no longer returned from API methods. ## [3.0.0] - 2020-07-28 From b94124e98fd1fb2ccd002de47b27840001a5a0a9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 12:20:20 +0200 Subject: [PATCH 72/74] Deprecate callback parameter --- lib/tumblr.js | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/tumblr.js b/lib/tumblr.js index 5098bd1d..8cb18de3 100644 --- a/lib/tumblr.js +++ b/lib/tumblr.js @@ -192,7 +192,7 @@ class TumblrClient { * * @param {string} apiPath - URL path for the request * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - request callback + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -370,9 +370,9 @@ class TumblrClient { * * @param {string} apiPath - URL path for the request * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used + * @return {Promise|undefined} Promise if no callback was provided */ postRequest(apiPath, paramsOrCallback, callback) { let params = paramsOrCallback; @@ -414,7 +414,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -429,7 +429,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -441,7 +441,7 @@ class TumblrClient { * Likes a post as the authenticating user * * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -453,7 +453,7 @@ class TumblrClient { * Unlikes a post as the authenticating user * * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -465,7 +465,7 @@ class TumblrClient { * Follows a blog as the authenticating user * * @param {{url: string}|{email:string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -477,7 +477,7 @@ class TumblrClient { * Unfollows a blog as the authenticating user * * @param {{url: string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -490,7 +490,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{id:string}} params - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -505,7 +505,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {Record} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -518,7 +518,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -531,7 +531,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -544,7 +544,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -557,7 +557,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form */ blogPosts(blogIdentifier, paramsOrCallback, callback) { let type = undefined; @@ -573,7 +573,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -586,7 +586,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -599,7 +599,7 @@ class TumblrClient { * * @param {string} blogIdentifier - blog name or URL * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -629,7 +629,7 @@ class TumblrClient { /** * Gets information about the authenticating user and their blogs * - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -641,7 +641,7 @@ class TumblrClient { * Gets the dashboard posts for the authenticating user * * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -653,7 +653,7 @@ class TumblrClient { * Gets the blogs the authenticating user follows * * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -665,7 +665,7 @@ class TumblrClient { * Gets the likes for the authenticating user * * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ @@ -677,7 +677,7 @@ class TumblrClient { * Gets posts tagged with the specified tag * * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes + * @param {TumblrClientCallback} [callback] **Deprecated** Omit the callback and use the promise form * * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used */ From 05f2470b2942f706641551244d4a419619aefdd1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 12:26:13 +0200 Subject: [PATCH 73/74] Generate declaration files on publish --- .gitignore | 1 + lib/tumblr.d.ts | 513 ------------------------------------------------ package.json | 4 +- 3 files changed, 4 insertions(+), 514 deletions(-) delete mode 100644 lib/tumblr.d.ts diff --git a/.gitignore b/.gitignore index b7e6a4f8..d1a0fea0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules credentials*.json .nyc_output *.tsbuildinfo +*.d.ts diff --git a/lib/tumblr.d.ts b/lib/tumblr.d.ts deleted file mode 100644 index dcece478..00000000 --- a/lib/tumblr.d.ts +++ /dev/null @@ -1,513 +0,0 @@ -export type Options = { - /** - * OAuth1 credential. Required for API key auth endpoints. - */ - consumer_key?: string; - /** - * OAuth1 credential. Required for OAuth endpoints. - */ - consumer_secret?: string; - /** - * OAuth1 credential. Required for OAuth endpoints. - */ - token?: string; - /** - * OAuth1 credential. Required for Oauth endpoints. - */ - token_secret?: string; - /** - * (optional) The API url if different from the default. - */ - baseUrl?: string; - /** - * (optional) Use promises instead of callbacks. - */ - returnPromises?: boolean; -}; -/** - * Handles the response from a client reuest - */ -export type TumblrClientCallback = ( - err: Error | null, - resp: any | null, - response?: http.IncomingMessage | null | undefined, -) => void; -/** - * @typedef Options - * @property {string} [consumer_key] OAuth1 credential. Required for API key auth endpoints. - * @property {string} [consumer_secret] OAuth1 credential. Required for OAuth endpoints. - * @property {string} [token] OAuth1 credential. Required for OAuth endpoints. - * @property {string} [token_secret] OAuth1 credential. Required for Oauth endpoints. - * @property {string} [baseUrl] (optional) The API url if different from the default. - * @property {boolean} [returnPromises] (optional) Use promises instead of callbacks. - */ -/** - * Handles the response from a client reuest - * - * @callback TumblrClientCallback - * @param {?Error} err - error message - * @param {?Object} resp - response body - * @param {?http.IncomingMessage} [response] - raw response - * @returns {void} - */ -declare class TumblrClient { - /** - * @typedef {Map|string>} RequestData - * - * @typedef {'text'|'quote'|'link'|'answer'|'video'|'audio'|'photo'|'chat'} PostType - * - * @typedef {'text'|'raw'} PostFormatFilter - * - * - * @typedef {{readonly auth:'none'}} NoneAuthCredentials - * @typedef {{readonly auth:'apiKey'; readonly apiKey:string}} ApiKeyCredentials - * @typedef {{readonly auth:'oauth1'; readonly consumer_key: string; readonly consumer_secret: string; readonly token: string; readonly token_secret: string }} OAuth1Credentials - * @typedef {NoneAuthCredentials|ApiKeyCredentials|OAuth1Credentials} Credentials - */ - /** - * Creates a Tumblr API client using the given options - * - * @param {Options} [options] - client options - * - * @constructor - */ - constructor(options?: Options | undefined); - /** - * Package version - * @type {typeof CLIENT_VERSION} - */ - version: typeof CLIENT_VERSION; - /** - * Base URL to API requests - * @type {string} - */ - baseUrl: string; - /** @type {Credentials} */ - credentials: - | { - readonly auth: 'none'; - } - | { - readonly auth: 'apiKey'; - readonly apiKey: string; - } - | { - readonly auth: 'oauth1'; - readonly consumer_key: string; - readonly consumer_secret: string; - readonly token: string; - readonly token_secret: string; - }; - /** @type {oauth.OAuth | null} */ - oauthClient: oauth.OAuth | null; - /** - * Performs a GET request - * - * @param {string} apiPath - URL path for the request - * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - request callback - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - getRequest( - apiPath: string, - paramsOrCallback?: Record | TumblrClientCallback | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * @template {TumblrClientCallback|undefined} CB - * - * @param {URL} url - * @param {'GET'|'POST'} method request method - * @param {null|RequestData} data - * @param {CB} providedCallback - * - * @returns {CB extends undefined ? Promise : undefined} - * - * @private - */ - private makeRequest; - /** - * Performs a POST request - * - * @param {string} apiPath - URL path for the request - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - postRequest( - apiPath: string, - paramsOrCallback?: Record | TumblrClientCallback | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * @deprecated Promises are returned if no callback is provided - */ - returnPromises(): void; - /** - * Creates a post on the given blog. - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @see {@link https://www.tumblr.com/docs/api/v2#posting|API Docs} - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - createPost( - blogIdentifier: string, - paramsOrCallback?: Record | TumblrClientCallback | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Edits a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record|TumblrClientCallback} [paramsOrCallback] - * @param {TumblrClientCallback} [callback] - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - editPost( - blogIdentifier: string, - paramsOrCallback?: Record | TumblrClientCallback | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Likes a post as the authenticating user - * - * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - likePost( - params: { - id: string; - reblog_key: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Unlikes a post as the authenticating user - * - * @param {{ id: string; reblog_key: string }} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unlikePost( - params: { - id: string; - reblog_key: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Follows a blog as the authenticating user - * - * @param {{url: string}|{email:string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - followBlog( - params: - | { - url: string; - } - | { - email: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Unfollows a blog as the authenticating user - * - * @param {{url: string}} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - unfollowBlog( - params: { - url: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Deletes a given post - * - * @param {string} blogIdentifier - blog name or URL - * @param {{id:string}} params - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - deletePost( - blogIdentifier: string, - params: { - id: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Reblogs a given post - * - * @deprecated Legacy post creation methods are deprecated. Use NPF methods. - * - * @param {string} blogIdentifier - blog name or URL - * @param {Record} params - parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - reblogPost( - blogIdentifier: string, - params: Record, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets information about a given blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{'fields[blogs]'?: string}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogInfo( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - 'fields[blogs]'?: string; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the likes for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogLikes( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - limit?: number; - offset?: number; - before?: number; - after?: number; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the followers for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogFollowers( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - limit?: number; - offset?: number; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets a list of posts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{type?:PostType; limit?: number; offset?: number}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - */ - blogPosts( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - type?: 'text' | 'quote' | 'link' | 'answer' | 'video' | 'audio' | 'photo' | 'chat'; - limit?: number; - offset?: number; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the queue for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{limit?: number; offset?: number; filter?: 'text'|'raw'}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogQueue( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - limit?: number; - offset?: number; - filter?: 'text' | 'raw'; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the drafts for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{before_id?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogDrafts( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - before_id?: number; - filter?: 'text' | 'raw'; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the submissions for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{offset?: number; filter?: PostFormatFilter}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogSubmissions( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - offset?: number; - filter?: 'text' | 'raw'; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the avatar URL for a blog - * - * @param {string} blogIdentifier - blog name or URL - * @param {{size?: 16|24|30|40|48|64|96|128|512}|TumblrClientCallback} [paramsOrCallback] - optional data sent with the request - * @param {TumblrClientCallback} [maybeCallback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - blogAvatar( - blogIdentifier: string, - paramsOrCallback?: - | TumblrClientCallback - | { - size?: 16 | 24 | 30 | 40 | 48 | 64 | 96 | 128 | 512; - } - | undefined, - maybeCallback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets information about the authenticating user and their blogs - * - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userInfo(callback?: TumblrClientCallback | undefined): Promise | undefined; - /** - * Gets the dashboard posts for the authenticating user - * - * @param {Record|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userDashboard( - paramsOrCallback?: Record | TumblrClientCallback | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the blogs the authenticating user follows - * - * @param {{limit?: number; offset?: number;}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userFollowing( - paramsOrCallback?: - | TumblrClientCallback - | { - limit?: number; - offset?: number; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets the likes for the authenticating user - * - * @param {{limit?: number; offset?: number; before?: number; after?: number}|TumblrClientCallback} [paramsOrCallback] - query parameters - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - userLikes( - paramsOrCallback?: - | TumblrClientCallback - | { - limit?: number; - offset?: number; - before?: number; - after?: number; - } - | undefined, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; - /** - * Gets posts tagged with the specified tag - * - * @param {{tag:string; [param:string]: string|number}} params - optional parameters sent with the request - * @param {TumblrClientCallback} [callback] - invoked when the request completes - * - * @return {Promise|undefined} Request object, or Promise if {@link returnPromises} was used - */ - taggedPosts( - params: { - [param: string]: string | number; - tag: string; - }, - callback?: TumblrClientCallback | undefined, - ): Promise | undefined; -} -declare const CLIENT_VERSION: '4.0.0-alpha.0'; -import oauth = require('oauth'); -export declare function createClient(options?: Options | undefined): TumblrClient; -export { TumblrClient as Client }; diff --git a/package.json b/package.json index 833eb833..9b90a338 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "test:coverage": "nyc npm run test", "test:integration": "mocha ./integration/read-only", "test:integration-write": "mocha ./integration/write", - "lint": "eslint --report-unused-disable-directives --ext 'js' --ext 'mjs' lib test integration" + "lint": "eslint --report-unused-disable-directives --ext 'js' --ext 'mjs' lib test integration", + "prepublishOnly": "tsc --build tsconfig.lib.json --force" }, "engines": { "node": ">=16", @@ -71,6 +72,7 @@ "typescript": "^5.1.6" }, "files": [ + "CHANGELOG.md", "/lib", "/LICENSE" ], From 6913bd061258fbb9fe8b86ff072922af4d3e75b6 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Aug 2023 12:39:06 +0200 Subject: [PATCH 74/74] Increase timeout for integration tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b90a338..c45fac38 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "mocha --timeout 100", "test:coverage": "nyc npm run test", "test:integration": "mocha ./integration/read-only", - "test:integration-write": "mocha ./integration/write", + "test:integration-write": "mocha ./integration/write --timeout=6000", "lint": "eslint --report-unused-disable-directives --ext 'js' --ext 'mjs' lib test integration", "prepublishOnly": "tsc --build tsconfig.lib.json --force" },