@prmichaelsen/acp-visualizer 0.1.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/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
# Third-Party API Integration Pattern
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: TanStack Start + Cloudflare Workers applications integrating with external APIs
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This pattern provides a structured approach for wrapping external APIs (property management systems, payment processors, search services, etc.) into modular, maintainable service modules. Each integration follows a consistent architecture: an auth/token module, an API client, domain-specific service methods, a type definitions file, and a barrel export.
|
|
12
|
+
|
|
13
|
+
The pattern ensures that external API complexity is contained within dedicated modules, with clean interfaces exposed to the rest of the application. It handles OAuth token lifecycle, sync operations, webhook processing, and error handling consistently across all integrations.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## When to Use This Pattern
|
|
18
|
+
|
|
19
|
+
✅ **Use this pattern when:**
|
|
20
|
+
- Integrating with external REST APIs (Guesty, Stripe, Algolia, Mailchimp, etc.)
|
|
21
|
+
- External API requires OAuth token management or API key rotation
|
|
22
|
+
- Need to sync external data into your database
|
|
23
|
+
- Processing webhooks from external services
|
|
24
|
+
- Multiple parts of your app consume the same external API
|
|
25
|
+
|
|
26
|
+
❌ **Don't use this pattern when:**
|
|
27
|
+
- Calling a single external endpoint once (inline fetch is fine)
|
|
28
|
+
- Using an SDK that already provides a clean interface
|
|
29
|
+
- The integration is purely client-side (e.g., Google Maps JS API)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Core Principles
|
|
34
|
+
|
|
35
|
+
1. **Modular File Structure**: Each integration gets its own directory with consistent file organization
|
|
36
|
+
2. **Barrel Exports**: Single `index.ts` entry point per integration
|
|
37
|
+
3. **Separated Concerns**: Auth, API client, domain services, types, and sync are separate files
|
|
38
|
+
4. **Token Lifecycle Management**: Token storage, refresh, and expiration handled in dedicated module
|
|
39
|
+
5. **Upsert Pattern for Sync**: External data synced via check-exists → update-or-create
|
|
40
|
+
6. **Sync Logging**: All sync operations logged to a dedicated collection for debugging
|
|
41
|
+
7. **Non-Blocking Errors**: Integration failures logged but don't crash the calling code
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Implementation
|
|
46
|
+
|
|
47
|
+
### Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/lib/
|
|
51
|
+
└── {integration}/
|
|
52
|
+
├── index.ts # Barrel exports
|
|
53
|
+
├── types.ts # External API type definitions
|
|
54
|
+
├── auth.ts # OAuth/token management
|
|
55
|
+
├── api-client.ts # HTTP client wrapper
|
|
56
|
+
├── token-storage.ts # Token persistence (Firestore/KV)
|
|
57
|
+
├── {domain}.ts # Domain service (listings, reservations, etc.)
|
|
58
|
+
├── sync.ts # Data sync logic
|
|
59
|
+
└── webhooks.ts # Webhook handler
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Code Example
|
|
63
|
+
|
|
64
|
+
#### Step 1: Define Types
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// src/lib/guesty/types.ts
|
|
68
|
+
|
|
69
|
+
export interface GuestyListing {
|
|
70
|
+
_id: string
|
|
71
|
+
title: string
|
|
72
|
+
nickname: string
|
|
73
|
+
address: {
|
|
74
|
+
full: string
|
|
75
|
+
city: string
|
|
76
|
+
state: string
|
|
77
|
+
zipcode: string
|
|
78
|
+
lat: number
|
|
79
|
+
lng: number
|
|
80
|
+
}
|
|
81
|
+
bedrooms: number
|
|
82
|
+
bathrooms: number
|
|
83
|
+
accommodates: number
|
|
84
|
+
picture: { thumbnail: string; regular: string }
|
|
85
|
+
prices: { basePrice: number; currency: string }
|
|
86
|
+
active: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface GuestyReservation {
|
|
90
|
+
_id: string
|
|
91
|
+
listingId: string
|
|
92
|
+
guestName: string
|
|
93
|
+
checkIn: string
|
|
94
|
+
checkOut: string
|
|
95
|
+
status: 'confirmed' | 'cancelled' | 'checked_in' | 'checked_out'
|
|
96
|
+
money: { totalPrice: number; currency: string }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface GuestyTokenResponse {
|
|
100
|
+
access_token: string
|
|
101
|
+
token_type: string
|
|
102
|
+
expires_in: number
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Step 2: Auth / Token Management
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// src/lib/guesty/auth.ts
|
|
110
|
+
|
|
111
|
+
import { GuestyTokenStorage } from './token-storage'
|
|
112
|
+
import type { GuestyTokenResponse } from './types'
|
|
113
|
+
|
|
114
|
+
export class GuestyAuthService {
|
|
115
|
+
private static tokenEndpoint = 'https://open-api.guesty.com/oauth2/token'
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get a valid access token, refreshing if needed
|
|
119
|
+
*/
|
|
120
|
+
static async getAccessToken(): Promise<string> {
|
|
121
|
+
// Check stored token
|
|
122
|
+
const stored = await GuestyTokenStorage.get()
|
|
123
|
+
if (stored && !this.isExpired(stored)) {
|
|
124
|
+
return stored.access_token
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Refresh token
|
|
128
|
+
return this.refreshToken()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Refresh the OAuth token
|
|
133
|
+
*/
|
|
134
|
+
static async refreshToken(): Promise<string> {
|
|
135
|
+
const response = await fetch(this.tokenEndpoint, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
138
|
+
body: new URLSearchParams({
|
|
139
|
+
grant_type: 'client_credentials',
|
|
140
|
+
client_id: process.env.GUESTY_CLIENT_ID!,
|
|
141
|
+
client_secret: process.env.GUESTY_CLIENT_SECRET!,
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Guesty token refresh failed: ${response.status}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data: GuestyTokenResponse = await response.json()
|
|
150
|
+
await GuestyTokenStorage.store({
|
|
151
|
+
access_token: data.access_token,
|
|
152
|
+
expires_at: Date.now() + data.expires_in * 1000,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return data.access_token
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private static isExpired(token: { expires_at: number }): boolean {
|
|
159
|
+
return Date.now() > token.expires_at - 60_000 // 1 minute buffer
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Step 3: API Client
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// src/lib/guesty/api-client.ts
|
|
168
|
+
|
|
169
|
+
import { GuestyAuthService } from './auth'
|
|
170
|
+
|
|
171
|
+
export class GuestyApiClient {
|
|
172
|
+
private static baseUrl = 'https://open-api.guesty.com/v1'
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Make an authenticated request to the Guesty API
|
|
176
|
+
*/
|
|
177
|
+
static async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
178
|
+
const token = await GuestyAuthService.getAccessToken()
|
|
179
|
+
|
|
180
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
181
|
+
...options,
|
|
182
|
+
headers: {
|
|
183
|
+
'Authorization': `Bearer ${token}`,
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
...options.headers,
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const error = await response.text()
|
|
191
|
+
throw new Error(`Guesty API error ${response.status}: ${error}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return response.json()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static async get<T>(path: string): Promise<T> {
|
|
198
|
+
return this.request<T>(path)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static async post<T>(path: string, body: unknown): Promise<T> {
|
|
202
|
+
return this.request<T>(path, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify(body),
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Step 4: Domain Service
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// src/lib/guesty/listings.ts
|
|
214
|
+
|
|
215
|
+
import { GuestyApiClient } from './api-client'
|
|
216
|
+
import type { GuestyListing } from './types'
|
|
217
|
+
|
|
218
|
+
export class GuestyListingsService {
|
|
219
|
+
/**
|
|
220
|
+
* Fetch all active listings from Guesty
|
|
221
|
+
*/
|
|
222
|
+
static async getListings(limit = 100): Promise<GuestyListing[]> {
|
|
223
|
+
const data = await GuestyApiClient.get<{ results: GuestyListing[] }>(
|
|
224
|
+
`/listings?limit=${limit}&fields=title,nickname,address,bedrooms,bathrooms,accommodates,picture,prices,active`
|
|
225
|
+
)
|
|
226
|
+
return data.results.filter(l => l.active)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Fetch a single listing by ID
|
|
231
|
+
*/
|
|
232
|
+
static async getListing(id: string): Promise<GuestyListing> {
|
|
233
|
+
return GuestyApiClient.get<GuestyListing>(`/listings/${id}`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Step 5: Sync Service
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// src/lib/guesty/sync.ts
|
|
242
|
+
|
|
243
|
+
import { GuestyListingsService } from './listings'
|
|
244
|
+
import { GuestyReservationsService } from './reservations'
|
|
245
|
+
import { PropertyDatabaseService } from '@/services/property-database.service'
|
|
246
|
+
import { ReservationDatabaseService } from '@/services/reservation-database.service'
|
|
247
|
+
|
|
248
|
+
interface SyncResult {
|
|
249
|
+
properties: { updated: number; created: number; errors: number }
|
|
250
|
+
reservations: { updated: number; created: number; errors: number }
|
|
251
|
+
syncedAt: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export class GuestySyncService {
|
|
255
|
+
/**
|
|
256
|
+
* Full sync: pull all listings and reservations from Guesty
|
|
257
|
+
*/
|
|
258
|
+
static async fullSync(): Promise<SyncResult> {
|
|
259
|
+
console.log('[GuestySync] Starting full sync...')
|
|
260
|
+
|
|
261
|
+
// Sync properties
|
|
262
|
+
const listings = await GuestyListingsService.getListings()
|
|
263
|
+
const propResult = await this.syncProperties(listings)
|
|
264
|
+
|
|
265
|
+
// Sync reservations
|
|
266
|
+
const reservations = await GuestyReservationsService.getReservations()
|
|
267
|
+
const resResult = await this.syncReservations(reservations)
|
|
268
|
+
|
|
269
|
+
const result: SyncResult = {
|
|
270
|
+
properties: propResult,
|
|
271
|
+
reservations: resResult,
|
|
272
|
+
syncedAt: new Date().toISOString(),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Log sync results
|
|
276
|
+
await this.logSync(result)
|
|
277
|
+
console.log('[GuestySync] Complete:', result)
|
|
278
|
+
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Upsert pattern: check if exists, update or create
|
|
284
|
+
*/
|
|
285
|
+
private static async syncProperties(listings: GuestyListing[]) {
|
|
286
|
+
let updated = 0, created = 0, errors = 0
|
|
287
|
+
|
|
288
|
+
for (const listing of listings) {
|
|
289
|
+
try {
|
|
290
|
+
const existing = await PropertyDatabaseService.getByExternalId(listing._id)
|
|
291
|
+
const mapped = this.mapListingToProperty(listing)
|
|
292
|
+
|
|
293
|
+
if (existing) {
|
|
294
|
+
await PropertyDatabaseService.update(existing.id, mapped)
|
|
295
|
+
updated++
|
|
296
|
+
} else {
|
|
297
|
+
await PropertyDatabaseService.create(mapped)
|
|
298
|
+
created++
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error(`[GuestySync] Failed to sync listing ${listing._id}:`, error)
|
|
302
|
+
errors++
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { updated, created, errors }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private static mapListingToProperty(listing: GuestyListing) {
|
|
310
|
+
return {
|
|
311
|
+
external_id: listing._id,
|
|
312
|
+
external_source: 'guesty',
|
|
313
|
+
title: listing.title,
|
|
314
|
+
address: listing.address.full,
|
|
315
|
+
bedrooms: listing.bedrooms,
|
|
316
|
+
bathrooms: listing.bathrooms,
|
|
317
|
+
capacity: listing.accommodates,
|
|
318
|
+
image_url: listing.picture?.regular,
|
|
319
|
+
base_price: listing.prices?.basePrice,
|
|
320
|
+
currency: listing.prices?.currency,
|
|
321
|
+
updated_at: new Date().toISOString(),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private static async logSync(result: SyncResult): Promise<void> {
|
|
326
|
+
// Store sync log for debugging and audit trail
|
|
327
|
+
await SyncLogDatabaseService.create({
|
|
328
|
+
source: 'guesty',
|
|
329
|
+
result,
|
|
330
|
+
created_at: new Date().toISOString(),
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Step 6: Barrel Export
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// src/lib/guesty/index.ts
|
|
340
|
+
|
|
341
|
+
export { GuestyAuthService } from './auth'
|
|
342
|
+
export { GuestyApiClient } from './api-client'
|
|
343
|
+
export { GuestyListingsService } from './listings'
|
|
344
|
+
export { GuestyReservationsService } from './reservations'
|
|
345
|
+
export { GuestySyncService } from './sync'
|
|
346
|
+
export { GuestyWebhooksService } from './webhooks'
|
|
347
|
+
export { GuestyTokenStorage } from './token-storage'
|
|
348
|
+
export type * from './types'
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### Step 7: Webhook Handler
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// routes/api/webhooks/guesty.tsx
|
|
355
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
356
|
+
import { GuestyWebhooksService } from '@/lib/guesty'
|
|
357
|
+
|
|
358
|
+
export const Route = createFileRoute('/api/webhooks/guesty')({
|
|
359
|
+
server: {
|
|
360
|
+
handlers: {
|
|
361
|
+
POST: async ({ request }) => {
|
|
362
|
+
try {
|
|
363
|
+
const body = await request.json()
|
|
364
|
+
const signature = request.headers.get('x-guesty-signature')
|
|
365
|
+
|
|
366
|
+
// Verify webhook signature
|
|
367
|
+
if (!GuestyWebhooksService.verifySignature(body, signature)) {
|
|
368
|
+
return new Response(JSON.stringify({ error: 'Invalid signature' }), {
|
|
369
|
+
status: 401,
|
|
370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Process webhook
|
|
375
|
+
await GuestyWebhooksService.handleWebhook(body)
|
|
376
|
+
|
|
377
|
+
return new Response(JSON.stringify({ received: true }), {
|
|
378
|
+
status: 200,
|
|
379
|
+
headers: { 'Content-Type': 'application/json' },
|
|
380
|
+
})
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('[Webhook] Guesty webhook error:', error)
|
|
383
|
+
// Return 200 to prevent webhook retries for processing errors
|
|
384
|
+
return new Response(JSON.stringify({ received: true, error: 'Processing failed' }), {
|
|
385
|
+
status: 200,
|
|
386
|
+
headers: { 'Content-Type': 'application/json' },
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Integration Checklist Template
|
|
398
|
+
|
|
399
|
+
For each new integration, create:
|
|
400
|
+
|
|
401
|
+
| File | Purpose |
|
|
402
|
+
|------|---------|
|
|
403
|
+
| `types.ts` | External API response types |
|
|
404
|
+
| `auth.ts` | Token management (OAuth, API keys) |
|
|
405
|
+
| `api-client.ts` | Authenticated HTTP client |
|
|
406
|
+
| `token-storage.ts` | Token persistence |
|
|
407
|
+
| `{domain}.ts` | Domain-specific operations |
|
|
408
|
+
| `sync.ts` | Data synchronization (if needed) |
|
|
409
|
+
| `webhooks.ts` | Webhook processing (if needed) |
|
|
410
|
+
| `index.ts` | Barrel exports |
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Benefits
|
|
415
|
+
|
|
416
|
+
### 1. Contained Complexity
|
|
417
|
+
All integration logic lives in one directory — easy to find, modify, or remove.
|
|
418
|
+
|
|
419
|
+
### 2. Consistent Pattern
|
|
420
|
+
Every integration follows the same structure — onboarding new integrations is predictable.
|
|
421
|
+
|
|
422
|
+
### 3. Token Lifecycle Managed
|
|
423
|
+
Token refresh, expiration, and storage are handled transparently by the auth module.
|
|
424
|
+
|
|
425
|
+
### 4. Testable
|
|
426
|
+
Each module can be unit tested independently. API client can be mocked for service tests.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Trade-offs
|
|
431
|
+
|
|
432
|
+
### 1. Boilerplate per Integration
|
|
433
|
+
**Downside**: Each integration requires 5-8 files.
|
|
434
|
+
**Mitigation**: The structure is consistent and can be scaffolded. Complexity is proportional to the API's complexity.
|
|
435
|
+
|
|
436
|
+
### 2. No SDK Reuse
|
|
437
|
+
**Downside**: Custom API clients instead of official SDKs.
|
|
438
|
+
**Mitigation**: Some SDKs don't work in Workers (Node.js-specific). Custom clients give full control over auth and error handling.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Anti-Patterns
|
|
443
|
+
|
|
444
|
+
### ❌ Anti-Pattern: Inline API Calls
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// ❌ BAD: Guesty calls scattered across codebase
|
|
448
|
+
const token = await fetch('https://open-api.guesty.com/oauth2/token', { ... })
|
|
449
|
+
const listings = await fetch('https://open-api.guesty.com/v1/listings', {
|
|
450
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// ✅ GOOD: Use integration module
|
|
454
|
+
import { GuestyListingsService } from '@/lib/guesty'
|
|
455
|
+
const listings = await GuestyListingsService.getListings()
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### ❌ Anti-Pattern: Swallowing Webhook Errors Silently
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// ❌ BAD: No logging on webhook failure
|
|
462
|
+
POST: async ({ request }) => {
|
|
463
|
+
try { await processWebhook(body) } catch {}
|
|
464
|
+
return Response.json({ received: true })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ✅ GOOD: Log errors, still return 200
|
|
468
|
+
POST: async ({ request }) => {
|
|
469
|
+
try { await processWebhook(body) } catch (error) {
|
|
470
|
+
console.error('[Webhook] Processing failed:', error)
|
|
471
|
+
}
|
|
472
|
+
return Response.json({ received: true })
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Related Patterns
|
|
479
|
+
|
|
480
|
+
- **[Library Services Pattern](./tanstack-cloudflare.library-services.md)**: Database services consume synced data
|
|
481
|
+
- **[Scheduled Tasks](./tanstack-cloudflare.scheduled-tasks.md)**: Token refresh and sync as cron jobs
|
|
482
|
+
- **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Webhook endpoints as API routes
|
|
483
|
+
- **[Zod Schema Validation](./tanstack-cloudflare.zod-schema-validation.md)**: Validate external API responses
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Checklist for Implementation
|
|
488
|
+
|
|
489
|
+
- [ ] Dedicated directory per integration (`src/lib/{integration}/`)
|
|
490
|
+
- [ ] `types.ts` with external API response types
|
|
491
|
+
- [ ] `auth.ts` with token management
|
|
492
|
+
- [ ] `api-client.ts` with authenticated HTTP wrapper
|
|
493
|
+
- [ ] `token-storage.ts` for token persistence
|
|
494
|
+
- [ ] Domain service files for specific operations
|
|
495
|
+
- [ ] `sync.ts` for data synchronization (if applicable)
|
|
496
|
+
- [ ] `webhooks.ts` for webhook handling (if applicable)
|
|
497
|
+
- [ ] `index.ts` barrel export
|
|
498
|
+
- [ ] Sync operations use upsert pattern
|
|
499
|
+
- [ ] Sync results logged to dedicated collection
|
|
500
|
+
- [ ] Webhook endpoints return 200 even on processing errors
|
|
501
|
+
- [ ] Token refresh handles expiration with buffer time
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
**Status**: Stable - Proven pattern for external API integrations
|
|
506
|
+
**Recommendation**: Use for all external API integrations beyond trivial single-endpoint calls
|
|
507
|
+
**Last Updated**: 2026-02-28
|
|
508
|
+
**Contributors**: Patrick Michaelsen
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Toast System
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: All success/error/warning/info feedback for user actions
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The toast system wraps `@prmichaelsen/pretty-toasts` with two hooks: `useToast` for direct toast calls and `useActionToast` for wrapping async operations with automatic success/error feedback. Toasts render at z-60 (above modals) via a `StandaloneToastContainer` in the root layout.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### useToast (Direct Toasts)
|
|
18
|
+
|
|
19
|
+
**File**: `src/hooks/useToast.ts`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
|
|
24
|
+
toast.success({ title: 'Saved!', message: 'Your changes have been saved.' })
|
|
25
|
+
toast.error({ title: 'Failed', message: 'Could not save changes.' })
|
|
26
|
+
toast.warning({ title: 'Warning', message: 'This action is irreversible.' })
|
|
27
|
+
toast.info({ title: 'Info', message: 'New version available.' })
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Toast Options**:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface ToastOptions {
|
|
34
|
+
title: string
|
|
35
|
+
message?: string
|
|
36
|
+
duration?: number // default: 2500ms
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### useActionToast (Async Action Wrapper)
|
|
41
|
+
|
|
42
|
+
**File**: `src/hooks/useActionToast.ts`
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const { withToast } = useActionToast()
|
|
46
|
+
|
|
47
|
+
const result = await withToast(
|
|
48
|
+
async () => {
|
|
49
|
+
await SomeService.doThing()
|
|
50
|
+
return { id: '123' }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
success: { title: 'Done!', message: 'Thing completed.' },
|
|
54
|
+
error: { title: 'Failed', message: 'Could not do thing.' },
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
// result is the action's return value on success, undefined on error
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Behavior**:
|
|
61
|
+
- Calls the async action
|
|
62
|
+
- On success: shows success toast, returns action result
|
|
63
|
+
- On error: shows error toast, returns `undefined`
|
|
64
|
+
- No try/catch needed at call site
|
|
65
|
+
|
|
66
|
+
### Root Layout Integration
|
|
67
|
+
|
|
68
|
+
**File**: `src/routes/__root.tsx`
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
<ToastProvider>
|
|
72
|
+
<AuthProvider>
|
|
73
|
+
{/* ... all other providers ... */}
|
|
74
|
+
{children}
|
|
75
|
+
</AuthProvider>
|
|
76
|
+
<StandaloneToastContainer /> {/* z-60, above modals */}
|
|
77
|
+
</ToastProvider>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
### Inline Action with Toast
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const handleDelete = async () => {
|
|
88
|
+
await withToast(
|
|
89
|
+
() => MemoryService.deleteMemory(memoryId),
|
|
90
|
+
{
|
|
91
|
+
success: { title: 'Deleted', message: 'Memory moved to trash.' },
|
|
92
|
+
error: { title: 'Delete failed' },
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Direct Toast for Non-Async Feedback
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const handleCopy = () => {
|
|
102
|
+
navigator.clipboard.writeText(url)
|
|
103
|
+
toast.success({ title: 'Copied!', message: 'Link copied to clipboard.' })
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Anti-Patterns
|
|
110
|
+
|
|
111
|
+
### Manual Try/Catch + Toast
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Bad: Verbose boilerplate
|
|
115
|
+
try {
|
|
116
|
+
await SomeService.doThing()
|
|
117
|
+
toast.success({ title: 'Done!' })
|
|
118
|
+
} catch (err) {
|
|
119
|
+
toast.error({ title: 'Failed', message: err.message })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Good: Use withToast
|
|
123
|
+
await withToast(() => SomeService.doThing(), {
|
|
124
|
+
success: { title: 'Done!' },
|
|
125
|
+
error: { title: 'Failed' },
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Checklist
|
|
132
|
+
|
|
133
|
+
- [ ] Use `withToast` for all async user actions (save, delete, publish, etc.)
|
|
134
|
+
- [ ] Use `toast.success/error` directly for synchronous feedback (copy, toggle)
|
|
135
|
+
- [ ] Toast container is rendered once in root layout (not per-component)
|
|
136
|
+
- [ ] Keep toast messages concise — title is required, message is optional
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
**Status**: Stable
|
|
141
|
+
**Last Updated**: 2026-03-14
|
|
142
|
+
**Contributors**: Community
|