@magicpages/ghost-typesense-webhook 0.0.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/.eslintrc.json +4 -0
- package/CHANGELOG.md +46 -0
- package/README.md +107 -0
- package/netlify.toml +16 -0
- package/package.json +36 -0
- package/src/__tests__/handler.test.ts +175 -0
- package/src/handler.ts +146 -0
package/.eslintrc.json
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @magicpages/ghost-typesense-webhook
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 3d371be: Initial release of Ghost Typesense integration packages:
|
|
8
|
+
- Type-safe configuration and utilities
|
|
9
|
+
- Core functionality for Ghost-Typesense synchronization
|
|
10
|
+
- CLI tool for content management
|
|
11
|
+
- Webhook handler for real-time updates
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [3d371be]
|
|
16
|
+
- @magicpages/ghost-typesense-config@2.0.0
|
|
17
|
+
- @magicpages/ghost-typesense-core@2.0.0
|
|
18
|
+
|
|
19
|
+
## 2.0.0
|
|
20
|
+
|
|
21
|
+
### Major Changes
|
|
22
|
+
|
|
23
|
+
- 35c8344: Initial release of Ghost Typesense integration packages:
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [35c8344]
|
|
28
|
+
- @magicpages/ghost-typesense-config@2.0.0
|
|
29
|
+
- @magicpages/ghost-typesense-core@2.0.0
|
|
30
|
+
|
|
31
|
+
## 1.0.0
|
|
32
|
+
|
|
33
|
+
### Major Changes
|
|
34
|
+
|
|
35
|
+
- ddd5315: Initial release of the Ghost Typesense integration packages:
|
|
36
|
+
|
|
37
|
+
- CLI tool for managing Ghost content in Typesense
|
|
38
|
+
- Webhook handler for real-time content synchronization
|
|
39
|
+
- Core functionality for Ghost-Typesense integration
|
|
40
|
+
- Configuration types and utilities
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [ddd5315]
|
|
45
|
+
- @magicpages/ghost-typesense-core@1.0.0
|
|
46
|
+
- @magicpages/ghost-typesense-config@1.0.0
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Ghost Typesense Webhook Handler
|
|
2
|
+
|
|
3
|
+
A production-ready Netlify Function that keeps your [Typesense](https://typesense.org/) search index synchronized with your [Ghost](https://ghost.org/) blog content in real-time. This webhook handler automatically processes content updates from Ghost and reflects them in your Typesense search index.
|
|
4
|
+
|
|
5
|
+
[](https://app.netlify.com/start/deploy?repository=https://github.com/magicpages/ghost-typesense)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
The webhook handler provides seamless integration between Ghost and Typesense. It enables real-time content synchronization and automatic handling of post publishing, updates, unpublishing, and deleting. The handler implements type-safe request processing with runtime validation and includes comprehensive error handling and logging capabilities.
|
|
10
|
+
|
|
11
|
+
## Deployment
|
|
12
|
+
|
|
13
|
+
### Option 1: One-Click Deploy (Recommended)
|
|
14
|
+
|
|
15
|
+
1. Click the "Deploy to Netlify" button above
|
|
16
|
+
2. Connect your GitHub account
|
|
17
|
+
3. Configure the required environment variables
|
|
18
|
+
4. Deploy the function
|
|
19
|
+
|
|
20
|
+
### Option 2: Manual Deployment
|
|
21
|
+
|
|
22
|
+
1. Clone the repository:
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/magicpages/ghost-typesense.git
|
|
25
|
+
cd ghost-typesense
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
2. Install dependencies:
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. Build the project:
|
|
34
|
+
```bash
|
|
35
|
+
npm run build
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
4. Deploy using the Netlify CLI:
|
|
39
|
+
```bash
|
|
40
|
+
netlify deploy --prod
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
### Environment Variables
|
|
46
|
+
|
|
47
|
+
Configure the following environment variables in your Netlify dashboard:
|
|
48
|
+
|
|
49
|
+
| Variable | Description | Example |
|
|
50
|
+
|----------|-------------|---------|
|
|
51
|
+
| `GHOST_URL` | Your Ghost blog URL | `https://blog.example.com` |
|
|
52
|
+
| `GHOST_CONTENT_API_KEY` | Ghost Content API key | `1234abcd...` |
|
|
53
|
+
| `TYPESENSE_HOST` | Typesense server host | `search.example.com` |
|
|
54
|
+
| `TYPESENSE_API_KEY` | Typesense API key (full API access, not search-only) | `xyz789...` |
|
|
55
|
+
| `COLLECTION_NAME` | Typesense collection name | `posts` |
|
|
56
|
+
| `WEBHOOK_SECRET` | A secure random string to validate webhook requests | `your-secret-key` |
|
|
57
|
+
|
|
58
|
+
### Ghost Integration Setup
|
|
59
|
+
|
|
60
|
+
1. Access your Ghost Admin panel
|
|
61
|
+
2. Navigate to Settings ā Integrations
|
|
62
|
+
3. Click "Add custom integration"
|
|
63
|
+
4. Name your integration (e.g., "Typesense Search")
|
|
64
|
+
5. Copy the Content API Key
|
|
65
|
+
6. Generate a secure random string for your webhook secret:
|
|
66
|
+
```bash
|
|
67
|
+
openssl rand -hex 32
|
|
68
|
+
```
|
|
69
|
+
7. Under Webhooks, add the following webhooks:
|
|
70
|
+
|
|
71
|
+
| Name | Event | Target URL |
|
|
72
|
+
|------|--------|------------|
|
|
73
|
+
| Post published | Post published | `https://your-netlify-site.netlify.app/.netlify/functions/handler?secret=your-secret-key` |
|
|
74
|
+
| Post updated | Post updated | `https://your-netlify-site.netlify.app/.netlify/functions/handler?secret=your-secret-key` |
|
|
75
|
+
| Post unpublished | Post unpublished | `https://your-netlify-site.netlify.app/.netlify/functions/handler?secret=your-secret-key` |
|
|
76
|
+
| Post deleted | Post deleted | `https://your-netlify-site.netlify.app/.netlify/functions/handler?secret=your-secret-key` |
|
|
77
|
+
|
|
78
|
+
Make sure to replace `your-secret-key` with the same secure random string you set in your Netlify environment variables.
|
|
79
|
+
|
|
80
|
+
## Local Development
|
|
81
|
+
|
|
82
|
+
1. Install dependencies:
|
|
83
|
+
```bash
|
|
84
|
+
npm install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
2. Create a `.env` file with your configuration:
|
|
88
|
+
```env
|
|
89
|
+
GHOST_URL=https://your-blog.ghost.io
|
|
90
|
+
GHOST_CONTENT_API_KEY=your_content_api_key
|
|
91
|
+
TYPESENSE_HOST=your_typesense_host
|
|
92
|
+
TYPESENSE_API_KEY=your_typesense_api_key
|
|
93
|
+
COLLECTION_NAME=posts
|
|
94
|
+
WEBHOOK_SECRET=your-development-secret
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
3. Start the development server:
|
|
98
|
+
```bash
|
|
99
|
+
npm run dev
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
4. Send webhook events to the local server from a local Ghost instance, including your secret:
|
|
103
|
+
`http://localhost:8888/.netlify/functions/handler?secret=your-development-secret`
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
package/netlify.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build]
|
|
2
|
+
command = "npm run build"
|
|
3
|
+
functions = "dist"
|
|
4
|
+
publish = "public"
|
|
5
|
+
|
|
6
|
+
[functions]
|
|
7
|
+
node_bundler = "esbuild"
|
|
8
|
+
included_files = ["dist/**"]
|
|
9
|
+
|
|
10
|
+
[template.environment]
|
|
11
|
+
GHOST_URL = "Your Ghost site URL"
|
|
12
|
+
GHOST_CONTENT_API_KEY = "Your Ghost Content API key"
|
|
13
|
+
TYPESENSE_HOST = "Your Typesense host"
|
|
14
|
+
TYPESENSE_API_KEY = "Your Typesense API key"
|
|
15
|
+
COLLECTION_NAME = "Name of your Typesense collection (default: posts)"
|
|
16
|
+
WEBHOOK_SECRET = "A secure random string to validate webhook requests"
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magicpages/ghost-typesense-webhook",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Webhook handler for real-time Ghost content synchronization with Typesense",
|
|
5
|
+
"author": "MagicPages",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/magicpages/ghost-typesense.git",
|
|
10
|
+
"directory": "apps/webhook-handler"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/handler.ts --format cjs --dts",
|
|
14
|
+
"clean": "rimraf dist",
|
|
15
|
+
"dev": "tsup src/handler.ts --format cjs --dts --watch",
|
|
16
|
+
"lint": "eslint src --ext .ts",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@magicpages/ghost-typesense-config": "*",
|
|
22
|
+
"@magicpages/ghost-typesense-core": "*",
|
|
23
|
+
"@netlify/functions": "^2.5.1",
|
|
24
|
+
"zod": "^3.22.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.11.17",
|
|
28
|
+
"tsup": "^8.0.1",
|
|
29
|
+
"rimraf": "^5.0.5",
|
|
30
|
+
"vitest": "^1.2.2",
|
|
31
|
+
"typescript": "^5.3.3"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
2
|
+
import type { HandlerEvent, HandlerResponse } from '@netlify/functions';
|
|
3
|
+
import { handler } from '../handler';
|
|
4
|
+
import { GhostTypesenseManager } from '@magicpages/ghost-typesense-core';
|
|
5
|
+
|
|
6
|
+
// Mock environment variables
|
|
7
|
+
const mockEnv = {
|
|
8
|
+
GHOST_URL: 'https://test.com',
|
|
9
|
+
GHOST_CONTENT_API_KEY: 'test-key',
|
|
10
|
+
TYPESENSE_HOST: 'localhost',
|
|
11
|
+
TYPESENSE_API_KEY: 'test-key',
|
|
12
|
+
COLLECTION_NAME: 'test-collection',
|
|
13
|
+
WEBHOOK_SECRET: 'test-secret'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
Object.entries(mockEnv).forEach(([key, value]) => {
|
|
17
|
+
vi.stubEnv(key, value);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Mock the core package
|
|
21
|
+
vi.mock('@magicpages/ghost-typesense-core', () => {
|
|
22
|
+
const indexPost = vi.fn().mockResolvedValue(undefined);
|
|
23
|
+
const deletePost = vi.fn().mockResolvedValue(undefined);
|
|
24
|
+
return {
|
|
25
|
+
GhostTypesenseManager: vi.fn().mockImplementation(() => ({
|
|
26
|
+
indexPost,
|
|
27
|
+
deletePost
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Webhook Handler', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createEvent = (overrides: Partial<HandlerEvent> = {}): HandlerEvent => ({
|
|
38
|
+
httpMethod: 'POST',
|
|
39
|
+
headers: {},
|
|
40
|
+
multiValueHeaders: {},
|
|
41
|
+
queryStringParameters: {},
|
|
42
|
+
multiValueQueryStringParameters: {},
|
|
43
|
+
path: '/',
|
|
44
|
+
body: null,
|
|
45
|
+
rawUrl: '',
|
|
46
|
+
rawQuery: '',
|
|
47
|
+
isBase64Encoded: false,
|
|
48
|
+
...overrides
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const parseResponse = (response: HandlerResponse | void): HandlerResponse => {
|
|
52
|
+
if (!response) {
|
|
53
|
+
throw new Error('Handler returned void');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...response,
|
|
57
|
+
body: response.body || ''
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const parseResponseBody = <T>(response: HandlerResponse): T => {
|
|
62
|
+
if (!response.body) {
|
|
63
|
+
throw new Error('Response body is empty');
|
|
64
|
+
}
|
|
65
|
+
return JSON.parse(response.body) as T;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mockContext = {
|
|
69
|
+
awsRequestId: '123',
|
|
70
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
71
|
+
functionName: 'test',
|
|
72
|
+
functionVersion: '1',
|
|
73
|
+
invokedFunctionArn: 'test',
|
|
74
|
+
memoryLimitInMB: '128',
|
|
75
|
+
logGroupName: 'test',
|
|
76
|
+
logStreamName: 'test',
|
|
77
|
+
identity: undefined,
|
|
78
|
+
clientContext: undefined,
|
|
79
|
+
getRemainingTimeInMillis: () => 1000,
|
|
80
|
+
done: () => {},
|
|
81
|
+
fail: () => {},
|
|
82
|
+
succeed: () => {}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
it('should return 401 if no secret provided', async () => {
|
|
86
|
+
const event = createEvent();
|
|
87
|
+
const response = parseResponse(await handler(event, mockContext));
|
|
88
|
+
|
|
89
|
+
expect(response.statusCode, 'Status code').toBe(401);
|
|
90
|
+
expect(parseResponseBody<{ error: string }>(response), 'Response body').toStrictEqual({ error: 'Missing webhook secret' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return 401 if invalid secret provided', async () => {
|
|
94
|
+
const event = createEvent({
|
|
95
|
+
queryStringParameters: { secret: 'wrong-secret' }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const response = parseResponse(await handler(event, mockContext));
|
|
99
|
+
|
|
100
|
+
expect(response.statusCode, 'Status code').toBe(401);
|
|
101
|
+
expect(parseResponseBody<{ error: string }>(response), 'Response body').toStrictEqual({ error: 'Invalid webhook secret' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return 405 for non-POST requests', async () => {
|
|
105
|
+
const event = createEvent({
|
|
106
|
+
httpMethod: 'GET',
|
|
107
|
+
queryStringParameters: { secret: 'test-secret' }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const response = parseResponse(await handler(event, mockContext));
|
|
111
|
+
|
|
112
|
+
expect(response.statusCode, 'Status code').toBe(405);
|
|
113
|
+
expect(parseResponseBody<{ error: string }>(response), 'Response body').toStrictEqual({ error: 'Method not allowed' });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should index post when published', async () => {
|
|
117
|
+
const event = createEvent({
|
|
118
|
+
queryStringParameters: { secret: 'test-secret' },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
post: {
|
|
121
|
+
current: {
|
|
122
|
+
id: 'test-post-1',
|
|
123
|
+
title: 'Test Post',
|
|
124
|
+
slug: 'test-post-1',
|
|
125
|
+
html: '<p>Test content</p>',
|
|
126
|
+
status: 'published',
|
|
127
|
+
visibility: 'public',
|
|
128
|
+
updated_at: '2024-02-09T12:00:00.000Z',
|
|
129
|
+
published_at: '2024-02-09T12:00:00.000Z',
|
|
130
|
+
custom_excerpt: 'Test excerpt',
|
|
131
|
+
feature_image: null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const response = parseResponse(await handler(event, mockContext));
|
|
138
|
+
|
|
139
|
+
expect(GhostTypesenseManager, 'Manager constructor').toHaveBeenCalledTimes(1);
|
|
140
|
+
const mockManager = (GhostTypesenseManager as unknown as Mock).mock.results[0]?.value;
|
|
141
|
+
expect(mockManager.indexPost, 'Index post').toHaveBeenCalledWith('test-post-1');
|
|
142
|
+
expect(response.statusCode, 'Status code').toBe(200);
|
|
143
|
+
expect(parseResponseBody<{ message: string }>(response), 'Response body').toStrictEqual({ message: 'Post indexed in Typesense' });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should delete post when unpublished', async () => {
|
|
147
|
+
const event = createEvent({
|
|
148
|
+
queryStringParameters: { secret: 'test-secret' },
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
post: {
|
|
151
|
+
current: {
|
|
152
|
+
id: 'test-post-1',
|
|
153
|
+
title: 'Test Post',
|
|
154
|
+
slug: 'test-post-1',
|
|
155
|
+
html: '<p>Test content</p>',
|
|
156
|
+
status: 'draft',
|
|
157
|
+
visibility: 'public',
|
|
158
|
+
updated_at: '2024-02-09T12:00:00.000Z',
|
|
159
|
+
published_at: '2024-02-09T12:00:00.000Z',
|
|
160
|
+
custom_excerpt: 'Test excerpt',
|
|
161
|
+
feature_image: null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const response = parseResponse(await handler(event, mockContext));
|
|
168
|
+
|
|
169
|
+
expect(GhostTypesenseManager, 'Manager constructor').toHaveBeenCalledTimes(1);
|
|
170
|
+
const mockManager = (GhostTypesenseManager as unknown as Mock).mock.results[0]?.value;
|
|
171
|
+
expect(mockManager.deletePost, 'Delete post').toHaveBeenCalledWith('test-post-1');
|
|
172
|
+
expect(response.statusCode, 'Status code').toBe(200);
|
|
173
|
+
expect(parseResponseBody<{ message: string }>(response), 'Response body').toStrictEqual({ message: 'Post removed from Typesense' });
|
|
174
|
+
});
|
|
175
|
+
});
|
package/src/handler.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Handler } from '@netlify/functions';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createDefaultConfig } from '@magicpages/ghost-typesense-config';
|
|
4
|
+
import { GhostTypesenseManager } from '@magicpages/ghost-typesense-core';
|
|
5
|
+
|
|
6
|
+
// Validate environment variables
|
|
7
|
+
const EnvSchema = z.object({
|
|
8
|
+
GHOST_URL: z.string().url(),
|
|
9
|
+
GHOST_CONTENT_API_KEY: z.string().min(1),
|
|
10
|
+
TYPESENSE_HOST: z.string().min(1),
|
|
11
|
+
TYPESENSE_API_KEY: z.string().min(1),
|
|
12
|
+
COLLECTION_NAME: z.string().min(1).default('posts'),
|
|
13
|
+
WEBHOOK_SECRET: z.string().min(1)
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Ghost webhook payload schema
|
|
17
|
+
const WebhookSchema = z.object({
|
|
18
|
+
post: z.object({
|
|
19
|
+
current: z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
slug: z.string(),
|
|
23
|
+
html: z.string(),
|
|
24
|
+
status: z.string(),
|
|
25
|
+
visibility: z.string(),
|
|
26
|
+
updated_at: z.string(),
|
|
27
|
+
published_at: z.string().nullable(),
|
|
28
|
+
custom_excerpt: z.string().nullable().optional(),
|
|
29
|
+
feature_image: z.string().nullable().optional(),
|
|
30
|
+
tags: z.array(z.object({
|
|
31
|
+
name: z.string()
|
|
32
|
+
})).optional(),
|
|
33
|
+
authors: z.array(z.object({
|
|
34
|
+
name: z.string()
|
|
35
|
+
})).optional()
|
|
36
|
+
}).optional(),
|
|
37
|
+
previous: z.object({
|
|
38
|
+
updated_at: z.string(),
|
|
39
|
+
html: z.string().optional(),
|
|
40
|
+
plaintext: z.string().optional()
|
|
41
|
+
}).optional()
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const handler: Handler = async (event) => {
|
|
46
|
+
try {
|
|
47
|
+
// Log request info
|
|
48
|
+
console.log('\nš Incoming webhook request');
|
|
49
|
+
console.log('š Method:', event.httpMethod);
|
|
50
|
+
|
|
51
|
+
// Validate environment variables
|
|
52
|
+
const env = EnvSchema.parse(process.env);
|
|
53
|
+
console.log('ā
Environment loaded successfully');
|
|
54
|
+
|
|
55
|
+
// Validate webhook secret
|
|
56
|
+
const secret = event.queryStringParameters?.secret;
|
|
57
|
+
if (!secret) {
|
|
58
|
+
console.log('ā No secret provided in request');
|
|
59
|
+
return {
|
|
60
|
+
statusCode: 401,
|
|
61
|
+
body: JSON.stringify({ error: 'Missing webhook secret' })
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (secret !== env.WEBHOOK_SECRET) {
|
|
66
|
+
console.log('š« Invalid secret provided');
|
|
67
|
+
return {
|
|
68
|
+
statusCode: 401,
|
|
69
|
+
body: JSON.stringify({ error: 'Invalid webhook secret' })
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('š Webhook secret validated');
|
|
74
|
+
|
|
75
|
+
// Create configuration
|
|
76
|
+
const config = createDefaultConfig(
|
|
77
|
+
env.GHOST_URL,
|
|
78
|
+
env.GHOST_CONTENT_API_KEY,
|
|
79
|
+
env.TYPESENSE_HOST,
|
|
80
|
+
env.TYPESENSE_API_KEY,
|
|
81
|
+
env.COLLECTION_NAME
|
|
82
|
+
);
|
|
83
|
+
console.log('āļø Configuration loaded');
|
|
84
|
+
|
|
85
|
+
// Initialize manager
|
|
86
|
+
const manager = new GhostTypesenseManager(config);
|
|
87
|
+
console.log('š Typesense manager initialized');
|
|
88
|
+
|
|
89
|
+
// Only process POST requests
|
|
90
|
+
if (event.httpMethod !== 'POST') {
|
|
91
|
+
console.log('ā ļø Invalid HTTP method:', event.httpMethod);
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 405,
|
|
94
|
+
body: JSON.stringify({ error: 'Method not allowed' })
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parse and validate webhook payload
|
|
99
|
+
if (!event.body) {
|
|
100
|
+
console.log('ā No request body provided');
|
|
101
|
+
throw new Error('No request body');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const webhook = WebhookSchema.parse(JSON.parse(event.body));
|
|
105
|
+
const { post } = webhook;
|
|
106
|
+
console.log('š¦ Webhook payload validated');
|
|
107
|
+
|
|
108
|
+
// Handle different webhook events based on post status changes
|
|
109
|
+
if (post.current) {
|
|
110
|
+
const { id, status, visibility, title } = post.current;
|
|
111
|
+
console.log(`š Processing post: "${title}" (${id})`);
|
|
112
|
+
|
|
113
|
+
if (status === 'published' && visibility === 'public') {
|
|
114
|
+
console.log('š Indexing published post');
|
|
115
|
+
await manager.indexPost(id);
|
|
116
|
+
console.log('⨠Post indexed successfully');
|
|
117
|
+
return {
|
|
118
|
+
statusCode: 200,
|
|
119
|
+
body: JSON.stringify({ message: 'Post indexed in Typesense' })
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
console.log('šļø Removing unpublished/private post');
|
|
123
|
+
await manager.deletePost(id);
|
|
124
|
+
console.log('⨠Post removed successfully');
|
|
125
|
+
return {
|
|
126
|
+
statusCode: 200,
|
|
127
|
+
body: JSON.stringify({ message: 'Post removed from Typesense' })
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('ā¹ļø No action required');
|
|
133
|
+
return {
|
|
134
|
+
statusCode: 200,
|
|
135
|
+
body: JSON.stringify({ message: 'No action required' })
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('ā Error processing webhook:', error);
|
|
139
|
+
return {
|
|
140
|
+
statusCode: 500,
|
|
141
|
+
body: JSON.stringify({ error: (error as Error).message })
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export { handler };
|