resend 0.1.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +45 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +5 -0
- package/build/src/interfaces.d.ts +19 -0
- package/build/src/interfaces.js +2 -0
- package/build/src/resend.d.ts +9 -0
- package/build/src/resend.js +57 -0
- package/build/src/resend.spec.d.ts +1 -0
- package/build/src/resend.spec.js +89 -0
- package/package.json +36 -17
- package/readme.md +82 -0
- package/.eslintrc +0 -6
- package/.npmignore +0 -7
- package/Dockerfile +0 -5
- package/README.md +0 -74
- package/bin/redeploy.sh +0 -13
- package/development/commit.sh +0 -9
- package/development/run.sh +0 -24
- package/docs/readme/main.png +0 -0
- package/docs/readme/main2.png +0 -0
- package/lib/index.js +0 -2
- package/lib/main.js +0 -132
- package/lib/spec.js +0 -50
- package/test/curl.sh +0 -9
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resend",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "build/src/index.js",
|
|
6
|
+
"typings": "build/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build/"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p .",
|
|
12
|
+
"lint": "tslint -p tsconfig.json -c tslint.json",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"prettier": "prettier \"src/**/*.{js,ts,tsx}\" --check",
|
|
16
|
+
"format": "prettier \"src/**/*.{js,ts,tsx}\" --write",
|
|
17
|
+
"prepublishOnly": "yarn run build"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/resend-inc/resend-node.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/resend-inc/resend-node/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/resend-inc/resend-node#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@react-email/render": "^0.0.1",
|
|
31
|
+
"axios": "0.27.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/jest": "28.1.7",
|
|
35
|
+
"@types/node": "18.6.4",
|
|
36
|
+
"@types/react": "18.0.17",
|
|
37
|
+
"axios-mock-adapter": "1.21.2",
|
|
38
|
+
"jest": "28.1.3",
|
|
39
|
+
"prettier": "2.7.1",
|
|
40
|
+
"supertest": "6.2.4",
|
|
41
|
+
"ts-jest": "28.0.8",
|
|
42
|
+
"tslint": "6.1.3",
|
|
43
|
+
"typescript": "4.7.4"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
export interface SendEmailData {
|
|
3
|
+
/** Sender email address. */
|
|
4
|
+
from: string;
|
|
5
|
+
/** Recipient email address. Max 50. */
|
|
6
|
+
to: string | string[];
|
|
7
|
+
/** Bcc recipient email address. Max 50. */
|
|
8
|
+
bcc?: string | string[];
|
|
9
|
+
/** Cc recipient email address. Max 50. */
|
|
10
|
+
cc?: string | string[];
|
|
11
|
+
/** Email subject. */
|
|
12
|
+
subject?: string;
|
|
13
|
+
/** The HTML version of the message. */
|
|
14
|
+
text?: string;
|
|
15
|
+
/** The plain text version of the message. */
|
|
16
|
+
html?: string;
|
|
17
|
+
/** The React version of the message. */
|
|
18
|
+
react?: ReactElement;
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SendEmailData } from './interfaces';
|
|
2
|
+
export declare class Resend {
|
|
3
|
+
readonly apiKey?: string | undefined;
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
private readonly headers;
|
|
6
|
+
private readonly request;
|
|
7
|
+
constructor(apiKey?: string | undefined);
|
|
8
|
+
sendEmail(data: SendEmailData): Promise<object>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.Resend = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const render_1 = require("@react-email/render");
|
|
18
|
+
const package_json_1 = require("../package.json");
|
|
19
|
+
class Resend {
|
|
20
|
+
constructor(apiKey) {
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
this.apiKey = process.env.RESEND_API_KEY;
|
|
24
|
+
if (!this.apiKey) {
|
|
25
|
+
throw new Error('Missing API key. Pass it to the constructor `new Resend("re_123")`');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this.apiKey = apiKey;
|
|
29
|
+
this.baseUrl = process.env.RESEND_BASE_URL || 'https://api.resend.com';
|
|
30
|
+
this.headers = {
|
|
31
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
32
|
+
'User-Agent': `node:${package_json_1.version}`,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
};
|
|
35
|
+
this.request = axios_1.default.create({
|
|
36
|
+
baseURL: this.baseUrl,
|
|
37
|
+
headers: this.headers,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
sendEmail(data) {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
try {
|
|
43
|
+
const path = `${this.baseUrl}/email`;
|
|
44
|
+
if (data.react) {
|
|
45
|
+
data.html = (0, render_1.render)(data.react);
|
|
46
|
+
delete data.react;
|
|
47
|
+
}
|
|
48
|
+
const response = yield this.request(path, { method: 'POST', data });
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.Resend = Resend;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const resend_1 = require("./resend");
|
|
16
|
+
const axios_mock_adapter_1 = __importDefault(require("axios-mock-adapter"));
|
|
17
|
+
const axios_1 = __importDefault(require("axios"));
|
|
18
|
+
const mock = new axios_mock_adapter_1.default(axios_1.default);
|
|
19
|
+
const resend = new resend_1.Resend('re_924b3rjh2387fbewf823');
|
|
20
|
+
describe('Resend', () => {
|
|
21
|
+
afterEach(() => mock.resetHistory());
|
|
22
|
+
it('throws API key error', () => {
|
|
23
|
+
expect(() => new resend_1.Resend()).toThrowErrorMatchingSnapshot();
|
|
24
|
+
});
|
|
25
|
+
it('sends email', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
const payload = {
|
|
27
|
+
from: 'bu@resend.com',
|
|
28
|
+
to: 'zeno@resend.com',
|
|
29
|
+
subject: 'Hello World',
|
|
30
|
+
};
|
|
31
|
+
mock.onPost('https://api.resend.com/email', payload).replyOnce(200, {
|
|
32
|
+
id: '1234',
|
|
33
|
+
from: 'bu@resend.com',
|
|
34
|
+
to: 'zeno@resend.com',
|
|
35
|
+
created_at: '123',
|
|
36
|
+
});
|
|
37
|
+
const data = yield resend.sendEmail(payload);
|
|
38
|
+
expect(data).toMatchSnapshot();
|
|
39
|
+
}));
|
|
40
|
+
it('sends email with multiple recipients', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
const payload = {
|
|
42
|
+
from: 'admin@resend.com',
|
|
43
|
+
to: ['bu@resend.com', 'zeno@resend.com'],
|
|
44
|
+
subject: 'Hello World',
|
|
45
|
+
};
|
|
46
|
+
mock.onPost('https://api.resend.com/email', payload).replyOnce(200, {
|
|
47
|
+
id: '1234',
|
|
48
|
+
from: 'admin@resend.com',
|
|
49
|
+
to: ['bu@resend.com', 'zeno@resend.com'],
|
|
50
|
+
created_at: '123',
|
|
51
|
+
});
|
|
52
|
+
const data = yield resend.sendEmail(payload);
|
|
53
|
+
expect(data).toMatchSnapshot();
|
|
54
|
+
}));
|
|
55
|
+
it('sends email with multiple bcc recipients', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
const payload = {
|
|
57
|
+
from: 'admin@resend.com',
|
|
58
|
+
to: 'bu@resend.com',
|
|
59
|
+
bcc: ['foo@resend.com', 'bar@resend.com'],
|
|
60
|
+
subject: 'Hello World',
|
|
61
|
+
};
|
|
62
|
+
mock.onPost('https://api.resend.com/email', payload).replyOnce(200, {
|
|
63
|
+
id: '1234',
|
|
64
|
+
from: 'admin@resend.com',
|
|
65
|
+
to: 'bu@resend.com',
|
|
66
|
+
bcc: ['foo@resend.com', 'bar@resend.com'],
|
|
67
|
+
created_at: '123',
|
|
68
|
+
});
|
|
69
|
+
const data = yield resend.sendEmail(payload);
|
|
70
|
+
expect(data).toMatchSnapshot();
|
|
71
|
+
}));
|
|
72
|
+
it('sends email with multiple cc recipients', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
+
const payload = {
|
|
74
|
+
from: 'admin@resend.com',
|
|
75
|
+
to: 'bu@resend.com',
|
|
76
|
+
cc: ['foo@resend.com', 'bar@resend.com'],
|
|
77
|
+
subject: 'Hello World',
|
|
78
|
+
};
|
|
79
|
+
mock.onPost('https://api.resend.com/email', payload).replyOnce(200, {
|
|
80
|
+
id: '1234',
|
|
81
|
+
from: 'admin@resend.com',
|
|
82
|
+
to: 'bu@resend.com',
|
|
83
|
+
cc: ['foo@resend.com', 'bar@resend.com'],
|
|
84
|
+
created_at: '123',
|
|
85
|
+
});
|
|
86
|
+
const data = yield resend.sendEmail(payload);
|
|
87
|
+
expect(data).toMatchSnapshot();
|
|
88
|
+
}));
|
|
89
|
+
});
|
package/package.json
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "build/src/index.js",
|
|
6
|
+
"typings": "build/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build/"
|
|
9
|
+
],
|
|
6
10
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
},
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"eslint": "^3.12.2"
|
|
11
|
+
"build": "tsc -p .",
|
|
12
|
+
"lint": "tslint -p tsconfig.json -c tslint.json",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"prettier": "prettier \"src/**/*.{js,ts,tsx}\" --check",
|
|
16
|
+
"format": "prettier \"src/**/*.{js,ts,tsx}\" --write",
|
|
17
|
+
"prepublishOnly": "yarn run build"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
21
|
+
"url": "git+https://github.com/resend-inc/resend-node.git"
|
|
21
22
|
},
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
22
25
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/resend-inc/resend-node/issues"
|
|
24
27
|
},
|
|
25
|
-
"homepage": "https://github.com/
|
|
26
|
-
|
|
28
|
+
"homepage": "https://github.com/resend-inc/resend-node#readme",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@react-email/render": "^0.0.1",
|
|
31
|
+
"axios": "0.27.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/jest": "28.1.7",
|
|
35
|
+
"@types/node": "18.6.4",
|
|
36
|
+
"@types/react": "18.0.17",
|
|
37
|
+
"axios-mock-adapter": "1.21.2",
|
|
38
|
+
"jest": "28.1.3",
|
|
39
|
+
"prettier": "2.7.1",
|
|
40
|
+
"supertest": "6.2.4",
|
|
41
|
+
"ts-jest": "28.0.8",
|
|
42
|
+
"tslint": "6.1.3",
|
|
43
|
+
"typescript": "4.7.4"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Resend Node.js SDK
|
|
2
|
+
|
|
3
|
+
Node.js library for the Resend API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install resend
|
|
9
|
+
# or
|
|
10
|
+
yarn add resend
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
First, you need to get an API key, which is available in the [Resend Dashboard](https://resend.com).
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { Resend } from 'resend';
|
|
19
|
+
const resend = new Resend('re_123');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Send your first email:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
await resend.sendEmail({
|
|
28
|
+
from: 'you@example.com',
|
|
29
|
+
to: 'user@gmail.com',
|
|
30
|
+
subject: 'hello world',
|
|
31
|
+
text: 'it works!',
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Send email using HTML
|
|
36
|
+
|
|
37
|
+
Send an email custom HTML content:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
await resend.sendEmail({
|
|
41
|
+
from: 'you@example.com',
|
|
42
|
+
to: 'user@gmail.com',
|
|
43
|
+
subject: 'hello world',
|
|
44
|
+
html: '<strong>it works!</strong>',
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Send email using React
|
|
49
|
+
|
|
50
|
+
Start by creating your email template as a React component.
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
import React from 'react'
|
|
54
|
+
|
|
55
|
+
export default function EmailTemplate(props) {
|
|
56
|
+
const { firstName, product } = props
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<h1>Welcome, {firstName}!</h1>
|
|
61
|
+
<p>Thanks for trying {product}. We’re thrilled to have you on board.</p>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then import the template component and pass it to the `react` property.
|
|
68
|
+
|
|
69
|
+
```jsx
|
|
70
|
+
import EmailTemplate from '../components/EmailTemplate'
|
|
71
|
+
|
|
72
|
+
await resend.sendEmail({
|
|
73
|
+
from: 'you@example.com',
|
|
74
|
+
to: 'user@gmail.com',
|
|
75
|
+
subject: 'hello world',
|
|
76
|
+
react: <EmailTemplate firstName="John" product="MyApp" />,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT License
|
package/.eslintrc
DELETED
package/.npmignore
DELETED
package/Dockerfile
DELETED
package/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# resend
|
|
2
|
-
|
|
3
|
-
Containerized utility to scan and print llen of Redis keys.
|
|
4
|
-
|
|
5
|
-
<img src="https://raw.githubusercontent.com/evanx/resend/master/docs/readme/main.png"/>
|
|
6
|
-
|
|
7
|
-
## Use case
|
|
8
|
-
|
|
9
|
-
We wish to expire a set of keys in Redis matching some pattern.
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Config
|
|
15
|
-
|
|
16
|
-
See `lib/config.js`
|
|
17
|
-
```javascript
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Docker
|
|
21
|
-
|
|
22
|
-
You can build as follows:
|
|
23
|
-
```shell
|
|
24
|
-
docker build -t resend https://github.com/evanx/resend.git
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
See `bin/test.sh` https://github.com/evanx/resend/blob/master/bin/test.sh
|
|
28
|
-
|
|
29
|
-
Builds:
|
|
30
|
-
- isolated network `resend-network`
|
|
31
|
-
- isolated Redis instance named `resend-redis`
|
|
32
|
-
- this utility `evanx/resend`
|
|
33
|
-
|
|
34
|
-
We populate our test keys:
|
|
35
|
-
```
|
|
36
|
-
populate() {
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
We build a container image for this service:
|
|
41
|
-
```
|
|
42
|
-
docker build -t resend https://github.com/evanx/resend.git
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
We interactively run the service on our test Redis container:
|
|
46
|
-
```
|
|
47
|
-
docker run --name resend-instance --rm -i \
|
|
48
|
-
--network=resend-network \
|
|
49
|
-
-e host=$redisHost \
|
|
50
|
-
-e pattern='*' \
|
|
51
|
-
resend
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Implementation
|
|
55
|
-
|
|
56
|
-
See `lib/main.js`
|
|
57
|
-
|
|
58
|
-
```javascript
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Appication archetype
|
|
62
|
-
|
|
63
|
-
Incidently `lib/index.js` uses the `redis-koa-app-rpf` application archetype.
|
|
64
|
-
```
|
|
65
|
-
require('redis-koa-app-rpf')(require('./spec'), require('./main'));
|
|
66
|
-
```
|
|
67
|
-
where we extract the `config` from `process.env` according to the `spec` and invoke our `main` function.
|
|
68
|
-
|
|
69
|
-
See https://github.com/evanx/redis-koa-app-rpf.
|
|
70
|
-
|
|
71
|
-
This provides lifecycle boilerplate to reuse across similar applications.
|
|
72
|
-
|
|
73
|
-
<hr>
|
|
74
|
-
https://twitter.com/@evanxsummers
|
package/bin/redeploy.sh
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
echo "NODE_ENV $NODE_ENV"
|
|
3
|
-
docker build -t geo-cache https://github.com/evanx/geo-cache.git
|
|
4
|
-
docker rm -f `docker ps -q -f name=geo-cache`
|
|
5
|
-
container=`docker run --name geo-cache -d \
|
|
6
|
-
--network=host --restart unless-stopped \
|
|
7
|
-
-e httpPort=8851 \
|
|
8
|
-
-e NODE_ENV=$NODE_ENV \
|
|
9
|
-
geo-cache`
|
|
10
|
-
sleep 1
|
|
11
|
-
echo "docker logs $container"
|
|
12
|
-
docker logs $container
|
|
13
|
-
|
package/development/commit.sh
DELETED
package/development/run.sh
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
set -u -x
|
|
3
|
-
|
|
4
|
-
mkdir -p tmp
|
|
5
|
-
|
|
6
|
-
redis-cli keys cache-geo-cache:* | xargs -n 1 redis-cli del
|
|
7
|
-
|
|
8
|
-
echo '
|
|
9
|
-
redis-cli keys cache-geo-cache:* | head
|
|
10
|
-
redis-cli keys cache-geo-cache:* | wc -l
|
|
11
|
-
' | sed '/^$/d' | tee tmp/keys.sh | dash -x
|
|
12
|
-
cat tmp/keys.sh
|
|
13
|
-
ls -l tmp/keys.sh
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
echo 'client sleep'
|
|
17
|
-
sleep 1
|
|
18
|
-
echo 'client curl'
|
|
19
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=20 Falstaff Close, Eynsham OX29 4QA'
|
|
20
|
-
redis-cli keys cache-geo-cache:*
|
|
21
|
-
) &
|
|
22
|
-
|
|
23
|
-
echo 'start server'
|
|
24
|
-
apiKey=$MAPS_API_KEY node --harmony lib/index.js
|
package/docs/readme/main.png
DELETED
|
Binary file
|
package/docs/readme/main2.png
DELETED
|
Binary file
|
package/lib/index.js
DELETED
package/lib/main.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
const fetch = require('node-fetch');
|
|
4
|
-
const h = require('render-html-rpf');
|
|
5
|
-
const lodash = require('lodash');
|
|
6
|
-
const multiExecAsync = require('multi-exec-async');
|
|
7
|
-
|
|
8
|
-
const reduceAllProperties = (object, fn) => Object.keys(object).reduce((result, key) => {
|
|
9
|
-
result[key] = fn(object[key]);
|
|
10
|
-
return result;
|
|
11
|
-
}, {});
|
|
12
|
-
|
|
13
|
-
module.exports = async appx => {
|
|
14
|
-
const {config, logger, client, api} = appx;
|
|
15
|
-
logger.info({config}, process.env.NODE_ENV);
|
|
16
|
-
api.get('/stats', async ctx => {
|
|
17
|
-
ctx.redirect('/metrics');
|
|
18
|
-
});
|
|
19
|
-
api.get('/metrics', async ctx => {
|
|
20
|
-
const [getCountRes, setCountRes, migrateCountRes] = await multiExecAsync(client, multi => {
|
|
21
|
-
multi.hgetall([config.redisNamespace, 'metric:get:path:count:h'].join(':'));
|
|
22
|
-
multi.hgetall([config.redisNamespace, 'metric:set:path:count:h'].join(':'));
|
|
23
|
-
multi.hgetall([config.redisNamespace, 'metric:migrate:path:count:h'].join(':'));
|
|
24
|
-
});
|
|
25
|
-
const getCount = reduceAllProperties(getCountRes || {}, value => parseInt(value));
|
|
26
|
-
const setCount = reduceAllProperties(setCountRes || {}, value => parseInt(value));
|
|
27
|
-
const migrateCount = reduceAllProperties(migrateCountRes || {}, value => parseInt(value));
|
|
28
|
-
const metrics = {getCount, setCount, migrateCount};
|
|
29
|
-
if (/(Mobile)/.test(ctx.get('user-agent'))) {
|
|
30
|
-
ctx.body = h.page({
|
|
31
|
-
title: 'gcache',
|
|
32
|
-
heading: 'Metrics',
|
|
33
|
-
content: [{
|
|
34
|
-
name: 'pre',
|
|
35
|
-
content: JSON.stringify(metrics, null, 2)}
|
|
36
|
-
],
|
|
37
|
-
footerLink: 'https://github.com/evanx/geo-cache'
|
|
38
|
-
});
|
|
39
|
-
} else {
|
|
40
|
-
ctx.body = metrics;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
api.get('/maps/api/*', async ctx => {
|
|
44
|
-
const path = ctx.params[0];
|
|
45
|
-
const url = 'https://maps.googleapis.com/maps/api/' + path;
|
|
46
|
-
const query = Object.keys(ctx.query)
|
|
47
|
-
.filter(key => key !== 'key')
|
|
48
|
-
.reduce((query, key) => {
|
|
49
|
-
query[key] = ctx.query[key];
|
|
50
|
-
return query;
|
|
51
|
-
}, {});
|
|
52
|
-
assert(!query.key);
|
|
53
|
-
const queryString = Object.keys(query).slice(0).sort().map(
|
|
54
|
-
key => [key, encodeURIComponent(query[key])].join('=')
|
|
55
|
-
).join('&');
|
|
56
|
-
const urlString = [url, queryString].join('?');
|
|
57
|
-
const authQuery = Object.assign({}, {key: config.apiKey}, ctx.query);
|
|
58
|
-
if (!authQuery.key) {
|
|
59
|
-
ctx.statusCode = 401;
|
|
60
|
-
const statusText = 'Unauthorized';
|
|
61
|
-
ctx.body = statusText + '\n';
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
logger.debug({url, query, urlString});
|
|
65
|
-
const sha = crypto.createHash('sha1').update(urlString).digest('hex');
|
|
66
|
-
const cacheKey = [config.redisNamespace, sha, 'j'].join(':');
|
|
67
|
-
if (true) {
|
|
68
|
-
const migrateSha = crypto.createHash('sha1').update(
|
|
69
|
-
[url, JSON.stringify(query)].join('#')
|
|
70
|
-
).digest('hex');
|
|
71
|
-
const migrateKey = ['cache-geo-cache', migrateSha, 'json'].join(':');
|
|
72
|
-
const [migrateContent] = await multiExecAsync(client, multi => {
|
|
73
|
-
multi.get(migrateKey);
|
|
74
|
-
});
|
|
75
|
-
if (migrateContent) {
|
|
76
|
-
await multiExecAsync(client, multi => {
|
|
77
|
-
multi.set(cacheKey, JSON.stringify(JSON.parse(migrateContent)));
|
|
78
|
-
multi.del(migrateKey);
|
|
79
|
-
multi.hincrby([config.redisNamespace, 'metric:migrate:path:count:h'].join(':'), path, 1);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
let [cachedContent] = await multiExecAsync(client, multi => {
|
|
84
|
-
multi.get(cacheKey);
|
|
85
|
-
multi.expire(cacheKey, config.expireSeconds);
|
|
86
|
-
multi.hincrby([config.redisNamespace, 'metric:get:path:count:h'].join(':'), path, 1);
|
|
87
|
-
});
|
|
88
|
-
if (cachedContent) {
|
|
89
|
-
logger.debug('hit', {url, sha, cacheKey});
|
|
90
|
-
const parsedContent = JSON.parse(cachedContent);
|
|
91
|
-
const formattedContent = JSON.stringify(parsedContent);
|
|
92
|
-
if (cachedContent !== formattedContent) {
|
|
93
|
-
logger.debug('reformat', cacheKey, cachedContent);
|
|
94
|
-
await multiExecAsync(client, multi => {
|
|
95
|
-
multi.set(cacheKey, formattedContent);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (lodash.includes(['OK', 'ZERO_RESULTS'], parsedContent.status)) {
|
|
99
|
-
logger.warn('hit', {url, sha, cacheKey, parsedContent});
|
|
100
|
-
ctx.set('Content-Type', 'application/json');
|
|
101
|
-
ctx.body = JSON.stringify(parsedContent, null, 2) + '\n';
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const urlQuery = url + '?' + Object.keys(authQuery)
|
|
106
|
-
.map(key => [key, encodeURIComponent(authQuery[key])].join('='))
|
|
107
|
-
.join('&');
|
|
108
|
-
const res = await fetch(urlQuery);
|
|
109
|
-
if (res.status !== 200) {
|
|
110
|
-
logger.debug('statusCode', url, res.status, res.statusText, query);
|
|
111
|
-
ctx.statusCode = res.status;
|
|
112
|
-
ctx.body = res.statusText + '\n';
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const fetchedContent = await res.json();
|
|
116
|
-
const formattedContent = JSON.stringify(fetchedContent, null, 2) + '\n';
|
|
117
|
-
ctx.set('Content-Type', 'application/json');
|
|
118
|
-
ctx.body = formattedContent;
|
|
119
|
-
if (!lodash.includes(['OK', 'ZERO_RESULTS'], fetchedContent.status)) {
|
|
120
|
-
logger.debug('status', fetchedContent.status, url);
|
|
121
|
-
} else {
|
|
122
|
-
const expireSeconds = lodash.includes(['ZERO_RESULTS'], fetchedContent.status)?
|
|
123
|
-
config.shortExpireSeconds:
|
|
124
|
-
config.expireSeconds;
|
|
125
|
-
logger.debug('expireSeconds', expireSeconds, fetchedContent.status, url);
|
|
126
|
-
await multiExecAsync(client, multi => {
|
|
127
|
-
multi.setex(cacheKey, expireSeconds, JSON.stringify(fetchedContent));
|
|
128
|
-
multi.hincrby([config.redisNamespace, 'metric:set:path:count:h'].join(':'), path, 1);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
package/lib/spec.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
description: 'Redis-based caching proxy for Google Maps API queries.',
|
|
3
|
-
required: {
|
|
4
|
-
redisHost: {
|
|
5
|
-
description: 'the Redis host',
|
|
6
|
-
default: 'localhost'
|
|
7
|
-
},
|
|
8
|
-
redisPort: {
|
|
9
|
-
description: 'the Redis port',
|
|
10
|
-
default: 6379
|
|
11
|
-
},
|
|
12
|
-
redisPassword: {
|
|
13
|
-
description: 'the Redis password',
|
|
14
|
-
required: false
|
|
15
|
-
},
|
|
16
|
-
redisNamespace: {
|
|
17
|
-
description: 'the Redis namespace',
|
|
18
|
-
default: 'cache-geo'
|
|
19
|
-
},
|
|
20
|
-
expireSeconds: {
|
|
21
|
-
description: 'the TTL for the cached content',
|
|
22
|
-
default: 21*24*3600
|
|
23
|
-
},
|
|
24
|
-
shortExpireSeconds: {
|
|
25
|
-
description: 'the TTL for the cached content',
|
|
26
|
-
default: 3*24*3600
|
|
27
|
-
},
|
|
28
|
-
httpPort: {
|
|
29
|
-
description: 'the HTTP port',
|
|
30
|
-
default: 8851
|
|
31
|
-
},
|
|
32
|
-
apiKey: {
|
|
33
|
-
description: 'our Google Maps API key',
|
|
34
|
-
required: false
|
|
35
|
-
},
|
|
36
|
-
loggerLevel: {
|
|
37
|
-
description: 'the logging level',
|
|
38
|
-
default: 'info',
|
|
39
|
-
example: 'debug'
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
test: {
|
|
43
|
-
loggerLevel: 'debug',
|
|
44
|
-
debug: true
|
|
45
|
-
},
|
|
46
|
-
development: {
|
|
47
|
-
loggerLevel: 'debug',
|
|
48
|
-
debug: true
|
|
49
|
-
}
|
|
50
|
-
}
|
package/test/curl.sh
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
set -u -x
|
|
3
|
-
|
|
4
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=20 Falstaff Close, Eynsham OX29 4QA'
|
|
5
|
-
curl 'http://localhost:8851/maps/api/geocode/json' -G --data-urlencode 'address=14 Grays Inn Road, Chancery Lane, London'
|
|
6
|
-
sleep 1
|
|
7
|
-
redis-cli keys cache-geo-cache:*
|
|
8
|
-
redis-cli hkeys cache-geo-cache:url:h
|
|
9
|
-
redis-cli hgetall cache-geo-cache:url:h
|