Merge pull request #821 from Radiergummi/feature/add-stripe-connection

feat(graphql): Add Stripe connection.
This commit is contained in:
Sam 2021-09-03 09:54:02 +02:00 committed by GitHub
commit 92a50b0621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 676 additions and 2 deletions

19
.pnp.cjs generated
View File

@ -4855,6 +4855,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["pg", "virtual:dddca670fd0b7758fb2e1b1a3e18ac7ebd1ecd06ecdd7acec2b78bccf1d35802cb22904bfbb233b16515a81f5cb819421786d20887823d98022b367036c1ad51#npm:8.6.0"],
["saslprep", "npm:1.0.3"],
["sqlite3", "virtual:dddca670fd0b7758fb2e1b1a3e18ac7ebd1ecd06ecdd7acec2b78bccf1d35802cb22904bfbb233b16515a81f5cb819421786d20887823d98022b367036c1ad51#npm:5.0.2"],
["stripe", "npm:8.172.0"],
["webpack", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:5.38.1"],
["webpack-cli", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:4.7.0"]
],
@ -6306,6 +6307,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],
"linkType": "HARD",
}],
["npm:16.7.10", {
"packageLocation": "./.yarn/cache/@types-node-npm-16.7.10-4e2e60d5a6-50c83528a9.zip/node_modules/@types/node/",
"packageDependencies": [
["@types/node", "npm:16.7.10"]
],
"linkType": "HARD",
}],
["npm:8.10.66", {
"packageLocation": "./.yarn/cache/@types-node-npm-8.10.66-b849acaf16-d4f105d5c9.zip/node_modules/@types/node/",
"packageDependencies": [
@ -26338,6 +26346,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["stripe", [
["npm:8.172.0", {
"packageLocation": "./.yarn/cache/stripe-npm-8.172.0-f088f5e7d6-4a578fa9e7.zip/node_modules/stripe/",
"packageDependencies": [
["stripe", "npm:8.172.0"],
["@types/node", "npm:16.7.10"],
["qs", "npm:6.10.1"]
],
"linkType": "HARD",
}]
]],
["strong-log-transformer", [
["npm:2.1.0", {
"packageLocation": "./.yarn/cache/strong-log-transformer-npm-2.1.0-45addd9278-46e84ece91.zip/node_modules/strong-log-transformer/",

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,139 @@
# Copyright 2020-2021 Lowdefy, Inc
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
_ref:
path: templates/general.yaml.njk
vars:
pageId: Stripe
pageTitle: Stripe
section: Connections
filePath: connections/Stripe.yaml
content:
- id: markdown
type: MarkdownWithCode
properties:
content: |
## Connections
Connection types:
- Stripe
### Stripe
[Stripe](https://stripe.com/) is a popular payment provider which allows you to accept payments, send payouts, and manage your business online.
The `Stripe` connector uses the official [Node.js client from Stripe](https://github.com/stripe/stripe-node).
In order to use the `Stripe` connection, you first need to create a [Stripe](https://stripe.com/) account and setup an API key.
> Secrets like API keys should be stored using the [`_secret`](operators/secret.md) operator.
#### Properties
- `secretKey: string`: __Required__ - Stripe [secret key](https://stripe.com/docs/keys).
- `apiVersion: string`: Stripe API version to use. Defaults to the account-wide version.
- `timeout: number`: Timeout for requests to the Stripe API.
- `maxNetworkRetries: number`: Maximum number of times failed requests are repeated before throwing an error.
- `telemetry: boolean`: Whether to send telemetry data to Stripe (this is forwarded to the Stripe client library. Lowdefy does not receive any telemetry data from your Stripe connection.)
#### Examples
###### Simple connection:
```yaml
connections:
- id: stripe
type: Stripe
properties:
secretKey:
_secret: STRIPE_SECRET_KEY
```
Environment variables:
```
LOWDEFY_SECRET_STRIPE_SECRET_KEY = sk_test_KyvNyie...
```
###### Using an older API version:
```yaml
connections:
- id: stripe
type: Stripe
properties:
secretKey:
_secret: STRIPE_SECRET_KEY
apiVersion: 2017-12-14
```
Environment variables:
```
LOWDEFY_SECRET_STRIPE_SECRET_KEY = sk_test_KyvNyie...
```
## Requests
Request types:
- StripeRequest
### StripeRequest
The `StripeRequest` request allows calls to all modules supported by the [Stripe API client](https://stripe.com/docs/api?lang=node) by nesting the resource and method calls:
```yaml
resource:
method:
- parameter1
- parameter2
```
#### Properties
- `{{ apiResource }}: object`: A Stripe API resource, eg. `customers`.
- `{{ method }}: array | null`: A resource method, eg. `create`. The arguments array will be passed on to the client method.
The Stripe client exposes all resources as objects, with the API methods being available as function properties on those resource objects.
In Lowdefy, you may access these properties by nesting them.
### Examples
###### List the 30 most recent customers
```yaml
requests:
- id: list_customers
type: StripeRequest
connectionId: stripe
properties:
customers:
list:
limit: 30
```
###### Create a payment intent
```yaml
requests:
- id: create_payment_intent
type: StripeRequest
connectionId: stripe
properties:
paymentIntents:
create:
- amount: 2000
currency: eur
payment_method_types: [ card ]
```
###### Retrieve a checkout session by ID
```yaml
requests:
- id: retrieve_checkout_session
type: StripeRequest
connectionId: stripe
properties:
checkout:
sessions:
retrieve:
- cs_test_onpT2icY2lrSU0IgDGXEhhcOHcWeJS5BpLcQGMx0uI9TZHLMBdzvWpvx
```

View File

@ -510,6 +510,9 @@
- id: SQLite
type: MenuLink
pageId: SQLite
- id: Stripe
type: MenuLink
pageId: Stripe
- id: actions
type: MenuGroup
properties:

View File

@ -134,6 +134,7 @@
- _ref: connections/PostgreSQL.yaml
- _ref: connections/SendGridMail.yaml
- _ref: connections/SQLite.yaml
- _ref: connections/Stripe.yaml
- _ref: actions/CallMethod.yaml
- _ref: actions/JsAction.yaml

View File

@ -65,7 +65,8 @@
"openid-client": "4.7.4",
"pg": "8.6.0",
"saslprep": "1.0.3",
"sqlite3": "5.0.2"
"sqlite3": "5.0.2",
"stripe": "8.172.0"
},
"devDependencies": {
"@babel/cli": "7.14.3",

View File

@ -0,0 +1,25 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import StripeRequest from './StripeRequest/StripeRequest';
import schema from './StripeSchema.json';
export default {
schema,
requests: {
StripeRequest,
},
};

View File

@ -0,0 +1,60 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { validate } from '@lowdefy/ajv';
import Stripe from './Stripe';
const { schema } = Stripe;
test('All requests are present', () => {
expect(Stripe.requests.StripeRequest).toBeDefined();
});
test('Valid connection schema, secretKey present', () => {
const connection = {
secretKey: 'foo',
};
expect(validate({ schema, data: connection })).toEqual({ valid: true });
});
test('Valid connection schema, secretKey present', () => {
const connection = {
secretKey: 'foo',
};
expect(validate({ schema, data: connection })).toEqual({ valid: true });
});
test('Valid connection schema, all options', () => {
const connection = {
secretKey: 'foo',
apiVersion: 'foo',
telemetry: true,
timeout: 42,
maxNetworkRetries: 42,
};
expect(validate({ schema, data: connection })).toEqual({ valid: true });
});
test('SecretKey missing', () => {
const connection = {};
expect(() => validate({ schema, data: connection })).toThrow(
'Stripe connection should have required property "secretKey".'
);
});

View File

@ -0,0 +1,49 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { get } from '@lowdefy/helpers';
import Stripe from 'stripe';
import schema from './StripeRequestSchema.json';
async function stripeRequest({ request, connection }) {
const stripe = new Stripe(connection.secretKey, {
apiVersion: connection.apiVersion,
maxNetworkRetries: connection.maxNetworkRetries,
timeout: connection.timeout,
telemetry: connection.telemetry,
});
let args = request;
const path = [];
do {
const key = Object.keys(args)[0];
path.push(key);
args = args[key];
} while (args && !Array.isArray(args));
const resource = get(stripe, path.slice(0, -1).join('.'));
const method = get(stripe, path.join('.'));
if (!resource || !method || typeof method !== 'function') {
throw new Error(`Invalid Stripe method ${path.join('.')}`);
}
return method.call(resource, ...(args || []));
}
export default { resolver: stripeRequest, schema, checkRead: false, checkWrite: false };

View File

@ -0,0 +1,238 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { validate } from '@lowdefy/ajv';
import StripeRequest from './StripeRequest';
const { resolver, schema } = StripeRequest;
const connection = {
secretKey: 'foo',
};
jest.mock('stripe', () => {
return {
__esModule: true,
default: function () {
return {
customers: {
list(...args) {
return args;
},
missing: 42,
},
};
},
};
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});
test('resource missing', () => {
const request = {};
expect(() => validate({ schema, data: request })).toThrow(
'StripeRequest should contain a resource to call.'
);
});
test('multiple resources', () => {
const request = {
accounts: {},
customers: {},
};
expect(() => validate({ schema, data: request })).toThrow(
'StripeRequest should contain only a single resource to call.'
);
});
test('invalid resource type', () => {
const request = {
accounts: 'foo',
};
expect(() => validate({ schema, data: request })).toThrow(
'StripeRequest resource should be an object.; StripeRequest resource should only contain a method to call, or sub-resource with a method to call.'
);
});
test('invalid method type', () => {
const request = {
accounts: {
foo: 'bar',
},
};
expect(() => validate({ schema, data: request })).toThrow(
'Should be an array of parameters or null.; StripeRequest resource should only contain a method to call, or sub-resource with a method to call.'
);
});
test('multiple methods', () => {
const request = {
accounts: {
foo: [],
bar: [],
},
};
expect(() => validate({ schema, data: request })).toThrow(
'StripeRequest resource should contain only a single method to call.'
);
});
test('invalid sub-resource type', () => {
const request = {
foo: {
bar: {
baz: 'quz',
},
},
};
expect(() => validate({ schema, data: request })).toThrow(
'Should be an array of parameters or null.; StripeRequest resource should only contain a method to call, or sub-resource with a method to call.'
);
});
test('multiple sub-resource methods', () => {
const request = {
foo: {
bar: {
baz: [],
quz: [],
},
},
};
expect(() => validate({ schema, data: request })).toThrow(
'Should be an array of parameters or null.; StripeRequest resource should only contain a method to call, or sub-resource with a method to call.'
);
});
test('valid request for resource method', () => {
const request = {
accounts: {
foo: [],
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for resource method without arguments', () => {
const request = {
accounts: {
foo: null,
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for resource method with arguments', () => {
const request = {
accounts: {
foo: ['bar', 42, true, { test: true }],
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for sub-resource method', () => {
const request = {
foo: {
bar: {
baz: [],
},
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for sub-resource method without arguments', () => {
const request = {
foo: {
bar: {
baz: null,
},
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for sub-resource method with arguments', () => {
const request = {
foo: {
bar: {
baz: ['bar', 42, true, { test: true }],
},
},
};
return expect(validate({ schema, data: request })).toEqual({ valid: true });
});
test('valid request for missing resource', async () => {
const request = {
foo: {
bar: [],
},
};
return expect(() => resolver({ request, connection })).rejects.toThrow(
'Invalid Stripe method foo.bar'
);
});
test('valid request for missing resource method', async () => {
const request = {
customers: {
foo: [],
},
};
return expect(() => resolver({ request, connection })).rejects.toThrow(
'Invalid Stripe method customers.foo'
);
});
test('valid request for missing resource method', async () => {
const request = {
customers: {
missing: [],
},
};
return expect(() => resolver({ request, connection })).rejects.toThrow(
'Invalid Stripe method customers.missing'
);
});
test('valid request', async () => {
const request = {
customers: {
list: ['foo', 42, { bar: true }],
},
};
const res = await resolver({ request, connection });
return expect(res).toEqual(['foo', 42, { bar: true }]);
});

View File

@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Request Schema - StripeRequest",
"type": "object",
"patternProperties": {
".+": {
"description": "Stripe API resource",
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"errorMessage": {
"type": "StripeRequest resource should be an object.",
"minProperties": "StripeRequest resource should contain a method to call.",
"maxProperties": "StripeRequest resource should contain only a single method to call.",
"oneOf": "StripeRequest resource should only contain a method to call, or sub-resource with a method to call."
},
"oneOf": [
{
"description": "Stripe API method to call on the resource",
"patternProperties": {
".+": {
"description": "Parameters to pass to the resource method, if any",
"type": [
"array",
"null"
],
"errorMessage": {
"type": "Should be an array of parameters or null."
}
}
}
},
{
"description": "Stripe API sub-resource of the parent resource",
"patternProperties": {
".+": {
"description": "Stripe API method to call on the resource",
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
".+": {
"description": "Parameters to pass to the sub-resource method, if any",
"type": [
"array",
"null"
]
}
}
}
}
}
]
}
},
"minProperties": 1,
"maxProperties": 1,
"errorMessage": {
"type": "StripeRequest request properties should be an object.",
"additionalProperties": "StripeRequest should contain a valid resource to call.",
"minProperties": "StripeRequest should contain a resource to call.",
"maxProperties": "StripeRequest should contain only a single resource to call."
}
}

View File

@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Lowdefy Connection Schema - Stripe",
"type": "object",
"required": [
"secretKey"
],
"properties": {
"secretKey": {
"type": "string",
"description": "Stripe secret key.",
"errorMessage": {
"type": "Stripe connection property \"secretKey\" should be a string."
}
},
"apiVersion": {
"type": "string",
"description": "Stripe API version to use.",
"default": null,
"errorMessage": {
"type": "Stripe connection property \"apiVersion\" should be a string."
}
},
"telemetry": {
"type": "boolean",
"description": "Allow Stripe to send latency telemetry.",
"default": true,
"errorMessage": {
"type": "Stripe connection property \"telemetry\" should be a boolean."
}
},
"timeout": {
"type": "integer",
"description": "Maximum time each request can take in ms.",
"default": 80000,
"errorMessage": {
"type": "Stripe connection property \"timeout\" should be an integer."
}
},
"maxNetworkRetries": {
"type": "integer",
"description": "The amount of times a request should be retried.",
"default": 0,
"errorMessage": {
"type": "Stripe connection property \"maxNetworkRetries\" should be an integer."
}
}
},
"errorMessage": {
"type": "Stripe connection properties should be an object.",
"required": {
"secretKey": "Stripe connection should have required property \"secretKey\"."
}
}
}

View File

@ -21,6 +21,7 @@ import GoogleSheet from './GoogleSheet/GoogleSheet';
import Knex from './Knex/Knex';
import MongoDBCollection from './MongoDBCollection/MongoDBCollection';
import SendGridMail from './SendGridMail/SendGridMail';
import Stripe from './Stripe/Stripe';
const resolvers = {
AwsS3Bucket,
@ -30,6 +31,7 @@ const resolvers = {
Knex,
MongoDBCollection,
SendGridMail,
Stripe,
};
export default resolvers;

View File

@ -3312,6 +3312,7 @@ __metadata:
pg: 8.6.0
saslprep: 1.0.3
sqlite3: 5.0.2
stripe: 8.172.0
webpack: 5.38.1
webpack-cli: 4.7.0
languageName: unknown
@ -4548,6 +4549,13 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:>=8.1.0":
version: 16.7.10
resolution: "@types/node@npm:16.7.10"
checksum: 50c83528a973746ba8fa67654c9fce33b57812ac9f67f445e56d8a11231587d866476323e7c1235894aca0b27321847915d058122b0f3f2e6e0acc20f54ba445
languageName: node
linkType: hard
"@types/node@npm:^10.1.0":
version: 10.17.60
resolution: "@types/node@npm:10.17.60"
@ -16853,7 +16861,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"qs@npm:^6.7.0, qs@npm:^6.9.4":
"qs@npm:^6.6.0, qs@npm:^6.7.0, qs@npm:^6.9.4":
version: 6.10.1
resolution: "qs@npm:6.10.1"
dependencies:
@ -19797,6 +19805,16 @@ resolve@^2.0.0-next.3:
languageName: node
linkType: hard
"stripe@npm:8.172.0":
version: 8.172.0
resolution: "stripe@npm:8.172.0"
dependencies:
"@types/node": ">=8.1.0"
qs: ^6.6.0
checksum: 4a578fa9e746d14990323bc5a93b3a95fe5af6f0a13d2efc682b038d41c2c7ca41073db78e68b6fe39d0dd8e6738edc420814cf042a2f509fb308c04c006d72f
languageName: node
linkType: hard
"strong-log-transformer@npm:^2.1.0":
version: 2.1.0
resolution: "strong-log-transformer@npm:2.1.0"