Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removed qs #2116

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^10.0.1",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"chai": "^4.3.6",
Expand All @@ -55,8 +54,7 @@
"nanoid": "^3.2.0"
},
"dependencies": {
"@types/node": ">=8.1.0",
"qs": "^6.11.0"
"@types/node": ">=8.1.0"
},
"license": "MIT",
"scripts": {
Expand Down Expand Up @@ -97,4 +95,4 @@
"require": "./cjs/stripe.cjs.node.js"
}
}
}
}
92 changes: 83 additions & 9 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as qs from 'qs';
import {
RequestData,
UrlInterpolator,
MultipartRequestData,
RequestArgs,
StripeResourceObject,
RequestData,
RequestHeaders,
MultipartRequestData,
StripeResourceObject,
UrlInterpolator,
} from './Types.js';

const OPTIONS_KEYS = [
Expand Down Expand Up @@ -39,16 +38,91 @@ export function isOptionsHash(o: unknown): boolean | unknown {
);
}

const has = Function.call.bind(Object.prototype.hasOwnProperty);

// https://github.com/sindresorhus/query-string/blob/main/base.js
const strictUriEncode = (str: string): string =>
encodeURIComponent(str).replace(
/[!'()*]/g,
(x) =>
`%${x
.charCodeAt(0)
.toString(16)
.toUpperCase()}`
);

/**
* Stringifies an Object into a query string
* @param obj - The object to stringify
* @param prefix - The parent key when nesting
* @param visited - Previously visited objects
* @returns The query string
*/
export function stringify (
obj: Record<string, unknown>,
config = {
serializeDate: (d: Date): string | number => d.toISOString(),
},
prefix?: string,
visited = new Set()
): string {
const str = [];
if (visited.has(obj)) {
visited.clear();
throw new RangeError('Cyclic object value');
}
visited.add(obj);
for (const p in obj) {
if (has(obj, p)) {
const k = prefix ? prefix + '[' + p + ']' : p;
const v = obj[p];
if (v === undefined) {
continue;
} else if (v instanceof Date) {
str.push(encodeURIComponent(k) + '=' + config.serializeDate(v));
} else if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) {
// eslint-disable-next-line max-depth
if (typeof v[i] === 'object' && v[i] !== null) {
str.push(
stringify(
v[i] as Record<string, unknown>,
config,
k + '[' + i + ']',
visited
)
);
} else {
str.push(
encodeURIComponent(k + '[' + i + ']') +
'=' +
strictUriEncode(v[i])
);
}
}
} else if (typeof v === 'object' && v !== null) {
str.push(stringify(v as Record<string, unknown>, config, k, visited));
} else {
str.push(
encodeURIComponent(k) + '=' + strictUriEncode((v as string) ?? '')
);
}
}
}
visited.delete(obj);
return str.join('&');
};

/**
* Stringifies an Object, accommodating nested objects
* (forming the conventional key 'parent[child]=value')
*/
export function stringifyRequestData(data: RequestData | string): string {
if (typeof data === 'string') return '';
return (
qs
.stringify(data, {
serializeDate: (d: Date) => Math.floor(d.getTime() / 1000).toString(),
})
stringify(data, {
serializeDate: (d: Date) => Math.floor(d.getTime() / 1000).toString(),
})
// Don't use strict form encoding by changing the square bracket control
// characters back to their literals. This is fine by the server, and
// makes these parameter strings easier to read.
Expand Down
2 changes: 1 addition & 1 deletion test/resources/OAuth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const stripe = require('../testUtils.js').getSpyableStripe();

const expect = require('chai').expect;
const URL = require('url');
const qs = require('qs');
const qs = require('node:querystring');

describe('OAuth', () => {
describe('authorize', () => {
Expand Down
160 changes: 159 additions & 1 deletion test/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
import { expect } from 'chai';
import * as utils from '../src/utils.js';
import {expect} from 'chai';

describe('utils', () => {
describe('makeURLInterpolator', () => {
Expand Down Expand Up @@ -472,6 +472,164 @@ describe('utils', () => {
expect(mergedBufToString).to.equal('foobar');
});
});

describe('stringify', function() {
it('stringifies a querystring object', function() {
expect(utils.stringify({a: 'b'})).to.equal('a=b');
expect(utils.stringify({a: 1})).to.equal('a=1');
expect(utils.stringify({a: 1, b: 2})).to.equal('a=1&b=2');
expect(utils.stringify({a: 'A_Z'})).to.equal('a=A_Z');
expect(utils.stringify({a: '€'})).to.equal('a=%E2%82%AC');
expect(utils.stringify({a: ''})).to.equal('a=%EE%80%80');
expect(utils.stringify({a: 'א'})).to.equal('a=%D7%90');
expect(utils.stringify({a: '𐐷'})).to.equal('a=%F0%90%90%B7');
});

it('stringifies falsy values', function() {
expect(utils.stringify(undefined), '');
expect(utils.stringify(null), '');
expect(utils.stringify(false), '');
expect(utils.stringify(0), '');
});
it('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function() {
expect(
utils.stringify({'name.obj': {first: 'John', last: 'Doe'}})
).to.equal('name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe');

expect(
utils.stringify({
'name.obj.subobject': {'first.godly.name': 'John', last: 'Doe'},
})
).to.equal(
'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe'
);
});

it('stringifies nested falsy values', function() {
expect(utils.stringify({a: {b: {c: null}}})).to.equal('a%5Bb%5D%5Bc%5D=');
expect(utils.stringify({a: {b: {c: false}}})).to.equal(
'a%5Bb%5D%5Bc%5D=false'
);
});

it('stringifies an array value', function() {
expect(utils.stringify({a: ['b', 'c', 'd']})).to.equal(
'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'
);
});

it('should omit object key/value pair when value is empty array', function() {
expect(utils.stringify({a: [], b: 'zz'})).to.equal('b=zz');
});

it('stringify an array with multiple items with a comma inside', function() {
expect(utils.stringify({a: ['b,c', 'd']})).to.equal(
'a%5B0%5D=b%2Cc&a%5B1%5D=d'
);
});
it('stringifies nested array', function() {
expect(utils.stringify({a: {b: ['c', 'd']}})).to.equal(
'a%5Bb%5D%5B0%5D=c&a%5Bb%5D%5B1%5D=d'
);
});
it('stringifies comma and empty array values', function() {
expect(utils.stringify({a: [',', '', 'c,d%']})).to.equal(
'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'
);
});

it('stringifies comma and empty non-array values', function() {
expect(utils.stringify({a: ',', b: '', c: 'c,d%'})).to.equal(
'a=%2C&b=&c=c%2Cd%25'
);
});

it('stringifies an object inside an array', function() {
expect(utils.stringify({a: [{b: 'c'}]})).to.equal('a%5B0%5D%5Bb%5D=c');
expect(utils.stringify({a: [{b: {c: [1]}}]})).to.equal(
'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1'
);
});

it('stringifies an array with mixed objects and primitives', function() {
expect(utils.stringify({a: [{b: 1}, 2, 3]})).to.equal(
'a%5B0%5D%5Bb%5D=1&a%5B1%5D=2&a%5B2%5D=3'
);
});

it('stringifies an empty value', function() {
expect(utils.stringify({a: ''})).to.equal('a=');
expect(utils.stringify({a: null})).to.equal('a=');
expect(utils.stringify({a: '', b: ''})).to.equal('a=&b=');
expect(utils.stringify({a: null, b: ''})).to.equal('a=&b=');
expect(utils.stringify({a: {b: ''}})).to.equal('a%5Bb%5D=');
expect(utils.stringify({a: {b: null}})).to.equal('a%5Bb%5D=');
});

it('stringifies a null object', function() {
const obj = Object.create(null);
obj.a = 'b';
expect(utils.stringify(obj)).to.equal('a=b');
});

it('returns an empty string for invalid input', function() {
expect(utils.stringify(undefined)).to.equal('');
expect(utils.stringify(false)).to.equal('');
expect(utils.stringify(null)).to.equal('');
expect(utils.stringify('')).to.equal('');
});

it('stringifies an object with a null object as a child', function() {
const obj = {a: Object.create(null)};
obj.a.b = 'c';
expect(utils.stringify(obj)).to.equal('a%5Bb%5D=c');
});

it('drops keys with a value of undefined', function() {
expect(utils.stringify({a: undefined})).to.equal('');
expect(utils.stringify({a: 'b', c: undefined})).to.equal('a=b');
expect(utils.stringify({a: {b: undefined, c: null}})).to.equal(
'a%5Bc%5D='
);
expect(utils.stringify({a: {b: undefined, c: ''}})).to.equal('a%5Bc%5D=');
});

it('url encodes values', function() {
expect(utils.stringify({a: 'b c'})).to.equal('a=b%20c');
expect(utils.stringify({a: 'b+c'})).to.equal('a=b%2Bc');
expect(utils.stringify({a: 'b&c'})).to.equal('a=b%26c');
expect(utils.stringify({a: 'b=c'})).to.equal('a=b%3Dc');
expect(utils.stringify({a: 'b%c'})).to.equal('a=b%25c');
});

it('stringifies a date', function() {
const now = new Date();
expect(utils.stringify({a: now})).to.equal(`a=${now.toISOString()}`);
});

it('stringifies the weird object from qs', function() {
expect(
utils.stringify({'my weird field': '~q1!2"\'w$5&7/z8)?'})
).to.equal('my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
});

it('skips properties that are part of the object prototype', function() {
// eslint-disable-next-line no-extend-native
Object.prototype.crash = 'test';

expect(utils.stringify({a: 'b'})).to.equal('a=b');
expect(utils.stringify({a: {b: 'c'}})).to.equal('a%5Bb%5D=c');

delete Object.prototype.crash; // Clean up
});

it('stringifies boolean values', function() {
expect(utils.stringify({a: true})).to.equal('a=true');
expect(utils.stringify({a: {b: true}})).to.equal('a%5Bb%5D=true');
expect(utils.stringify({b: false})).to.equal('b=false');
expect(utils.stringify({b: {c: false}})).to.equal('b%5Bc%5D=false');
});
});
});

function handleWarnings(doWithShimmedConsoleWarn, onWarn): void {
Expand Down