@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,126 @@
|
|
|
1
|
+
# Image Carousel
|
|
2
|
+
|
|
3
|
+
**Category**: Design
|
|
4
|
+
**Applicable To**: Horizontal image galleries in chat messages, profiles, and any scrollable media strip
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
A CSS scroll-snap carousel with IntersectionObserver-based visibility tracking, lazy loading, scale/opacity interpolation per slide, keyboard navigation, and responsive dot/counter navigation. Used for chat message image galleries, memory card carousels, and profile media.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Implementation
|
|
16
|
+
|
|
17
|
+
### ImageCarousel
|
|
18
|
+
|
|
19
|
+
**File**: `src/components/ImageCarousel.tsx`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface ImageCarouselProps {
|
|
23
|
+
images: Array<{ src: string; alt?: string; crop?: CropData | null }>
|
|
24
|
+
onImageClick?: (index: number) => void // Opens lightbox
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Core Mechanics**:
|
|
29
|
+
|
|
30
|
+
1. **CSS Scroll-Snap**:
|
|
31
|
+
```typescript
|
|
32
|
+
<div className="flex overflow-x-auto snap-x snap-mandatory"
|
|
33
|
+
style={{ scrollBehavior: 'smooth', WebkitOverflowScrolling: 'touch' }}>
|
|
34
|
+
{images.map((img, i) => (
|
|
35
|
+
<div key={i} className="flex-shrink-0 w-full snap-center">
|
|
36
|
+
<img src={img.src} />
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. **IntersectionObserver** (21-step threshold: 0/20 to 20/20):
|
|
43
|
+
- Tracks per-slide visibility ratio (0.0 to 1.0)
|
|
44
|
+
- Updates `slideVisibility` array on each intersection change
|
|
45
|
+
- Most-visible slide becomes `currentIndex`
|
|
46
|
+
|
|
47
|
+
3. **Scale/Opacity Interpolation**:
|
|
48
|
+
```typescript
|
|
49
|
+
const ratio = slideVisibility[i] ?? 0
|
|
50
|
+
const scale = 0.92 + 0.08 * ratio // 92% → 100%
|
|
51
|
+
const opacity = 0.4 + 0.6 * ratio // 40% → 100%
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
4. **Lazy Loading**:
|
|
55
|
+
- `loadedIndices` Set tracks rendered slides
|
|
56
|
+
- Preloads current ± 1 adjacent slides
|
|
57
|
+
- Unloaded slides render placeholder (prevents layout shift)
|
|
58
|
+
|
|
59
|
+
5. **Navigation UI**:
|
|
60
|
+
- **Dots**: For ≤7 slides, dot indicators at bottom (active = white, inactive = gray)
|
|
61
|
+
- **Counter**: For >7 slides, text "3 / 10"
|
|
62
|
+
- **Chevrons**: Desktop only (`hidden md:flex`), hidden at boundaries
|
|
63
|
+
|
|
64
|
+
6. **Keyboard**: ArrowLeft/ArrowRight scrolls to adjacent slide
|
|
65
|
+
|
|
66
|
+
**Touch Direction Detection** (MemoryCardCarousel variant):
|
|
67
|
+
```typescript
|
|
68
|
+
const handleTouchMove = (e: ReactTouchEvent) => {
|
|
69
|
+
const dx = Math.abs(e.touches[0].clientX - touchStart.x)
|
|
70
|
+
const dy = Math.abs(e.touches[0].clientY - touchStart.y)
|
|
71
|
+
// Lock horizontal scroll-snap when horizontal swipe detected
|
|
72
|
+
if (dx > dy * 2) {
|
|
73
|
+
scrollRef.current.style.scrollSnapType = 'x mandatory'
|
|
74
|
+
} else {
|
|
75
|
+
// Disable snap to allow vertical page scroll
|
|
76
|
+
scrollRef.current.style.scrollSnapType = 'none'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Anti-Patterns
|
|
84
|
+
|
|
85
|
+
### Using State for Slide Index Instead of IntersectionObserver
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Bad: Scroll events are noisy and imprecise
|
|
89
|
+
onScroll={(e) => setIndex(Math.round(e.target.scrollLeft / slideWidth))}
|
|
90
|
+
|
|
91
|
+
// Good: IntersectionObserver provides precise visibility ratios
|
|
92
|
+
const observer = new IntersectionObserver(entries => {
|
|
93
|
+
entries.forEach(entry => {
|
|
94
|
+
slideVisibility[index] = entry.intersectionRatio
|
|
95
|
+
})
|
|
96
|
+
}, { threshold: Array.from({ length: 21 }, (_, i) => i / 20) })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Rendering All Slides Eagerly
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Bad: All images load at once
|
|
103
|
+
{images.map((img) => <img src={img.src} />)}
|
|
104
|
+
|
|
105
|
+
// Good: Lazy load with preload ±1
|
|
106
|
+
{loadedIndices.has(i) ? <img src={img.src} /> : <div className="bg-gray-800" />}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Checklist
|
|
112
|
+
|
|
113
|
+
- [ ] Use `snap-x snap-mandatory` on scroll container
|
|
114
|
+
- [ ] Use `snap-center` on each slide
|
|
115
|
+
- [ ] Track visibility with IntersectionObserver (21-step threshold)
|
|
116
|
+
- [ ] Lazy-load slides with ±1 preloading
|
|
117
|
+
- [ ] Apply scale/opacity interpolation for smooth transitions
|
|
118
|
+
- [ ] Show dots for ≤7 slides, counter for >7
|
|
119
|
+
- [ ] Hide chevrons on mobile, show only at non-boundary positions
|
|
120
|
+
- [ ] Handle touch direction to avoid blocking vertical page scroll
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
**Status**: Stable
|
|
125
|
+
**Last Updated**: 2026-03-14
|
|
126
|
+
**Contributors**: Community
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# Library Services Pattern
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: TanStack Start + Cloudflare Workers applications with Firestore/database operations
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
All data access operations (API calls, Firestore operations, external services) must go through dedicated service layer libraries. This pattern provides a clear separation between client-side API wrappers and server-side database services, ensuring proper isolation, testability, and maintainability.
|
|
12
|
+
|
|
13
|
+
The pattern enforces that direct calls from components, routes, or other non-service code are anti-patterns. Instead, all data operations flow through well-defined service classes that handle validation, error logging, and type safety.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## When to Use This Pattern
|
|
18
|
+
|
|
19
|
+
✅ **Use this pattern when:**
|
|
20
|
+
- Building TanStack Start applications with server-side data access
|
|
21
|
+
- Working with Firestore or other database operations
|
|
22
|
+
- Need to separate client-side and server-side data access logic
|
|
23
|
+
- Want to ensure type safety and validation at service boundaries
|
|
24
|
+
- Building applications that require testable, mockable data layers
|
|
25
|
+
- Need consistent error handling and logging across data operations
|
|
26
|
+
|
|
27
|
+
❌ **Don't use this pattern when:**
|
|
28
|
+
- Building purely static sites with no data access
|
|
29
|
+
- Working on trivial prototypes or demos
|
|
30
|
+
- The overhead of service layers outweighs benefits (very simple CRUD apps)
|
|
31
|
+
- You have no server-side data access requirements
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Core Principles
|
|
36
|
+
|
|
37
|
+
1. **Service Layer Abstraction**: All data operations must be encapsulated in service classes that provide clean APIs for data access
|
|
38
|
+
2. **No Direct Database Calls**: Components and routes never call `getDocument`, `setDocument`, or `queryDocuments` directly
|
|
39
|
+
3. **No Direct API Calls**: Components never call `fetch('/api/...')` directly - they use API service wrappers
|
|
40
|
+
4. **Clear Naming Convention**: Service class names indicate scope - `DatabaseService` for server-side, `Service` for client-side
|
|
41
|
+
5. **Same Method Names**: Database and API services use identical method names for consistency
|
|
42
|
+
6. **Type Safety**: Services enforce Zod validation and return typed data models
|
|
43
|
+
7. **Error Handling**: Services centralize error logging and handling
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Implementation
|
|
48
|
+
|
|
49
|
+
### Structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/
|
|
53
|
+
├── services/
|
|
54
|
+
│ ├── {domain}-database.service.ts # Server-side database operations
|
|
55
|
+
│ ├── {domain}.service.ts # Client-side API wrappers
|
|
56
|
+
│ └── ...
|
|
57
|
+
├── routes/
|
|
58
|
+
│ └── api/
|
|
59
|
+
│ └── {domain}/
|
|
60
|
+
│ └── index.ts # API routes use DatabaseService
|
|
61
|
+
└── components/
|
|
62
|
+
└── {domain}/
|
|
63
|
+
└── Component.tsx # Components use Service (API wrapper)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Service Types
|
|
67
|
+
|
|
68
|
+
#### 1. Database Services (Server-Side)
|
|
69
|
+
|
|
70
|
+
**Purpose**: Direct Firestore/database operations
|
|
71
|
+
**Naming**: `{Domain}DatabaseService`
|
|
72
|
+
**File**: `{domain}-database.service.ts`
|
|
73
|
+
**Used By**: API routes, beforeLoad, server functions, cron jobs
|
|
74
|
+
|
|
75
|
+
**Characteristics**:
|
|
76
|
+
- Directly calls `getDocument`, `setDocument`, `queryDocuments`
|
|
77
|
+
- Server-side only (uses firebase-admin-sdk)
|
|
78
|
+
- Handles Zod validation
|
|
79
|
+
- Manages timestamps (created_at, updated_at)
|
|
80
|
+
- Returns typed data models
|
|
81
|
+
|
|
82
|
+
#### 2. API Services (Client Wrappers)
|
|
83
|
+
|
|
84
|
+
**Purpose**: Wrap API endpoint calls for client-side use
|
|
85
|
+
**Naming**: `{Domain}Service`
|
|
86
|
+
**File**: `{domain}.service.ts`
|
|
87
|
+
**Used By**: Components, client-side hooks
|
|
88
|
+
|
|
89
|
+
**Characteristics**:
|
|
90
|
+
- Calls `fetch('/api/...')`
|
|
91
|
+
- Client-side safe
|
|
92
|
+
- Handles HTTP errors
|
|
93
|
+
- Returns typed data models
|
|
94
|
+
|
|
95
|
+
### Code Example
|
|
96
|
+
|
|
97
|
+
#### Database Service (Server-Side)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// src/services/oauth-integration-database.service.ts
|
|
101
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8'
|
|
102
|
+
import { getUserOAuthIntegration } from '@/constant/collections'
|
|
103
|
+
import { OAuthIntegrationSchema, type OAuthIntegration } from '@/schemas/oauth-integration'
|
|
104
|
+
|
|
105
|
+
export class OAuthIntegrationDatabaseService {
|
|
106
|
+
static async getIntegration(userId: string, provider: string): Promise<OAuthIntegration | null> {
|
|
107
|
+
try {
|
|
108
|
+
const path = getUserOAuthIntegration(userId, provider)
|
|
109
|
+
const doc = await getDocument(path, 'current')
|
|
110
|
+
|
|
111
|
+
if (!doc) return null
|
|
112
|
+
|
|
113
|
+
const result = OAuthIntegrationSchema.safeParse(doc)
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
console.error(`Invalid OAuth integration data for ${provider}:`, result.error)
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result.data
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`Failed to get OAuth integration for ${provider}:`, error)
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static async saveIntegration(userId: string, provider: string, data: OAuthIntegrationInput): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
const path = getUserOAuthIntegration(userId, provider)
|
|
129
|
+
const now = new Date().toISOString()
|
|
130
|
+
|
|
131
|
+
const integration: OAuthIntegration = {
|
|
132
|
+
...data,
|
|
133
|
+
connected_at: now,
|
|
134
|
+
created_at: now,
|
|
135
|
+
updated_at: now,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await setDocument(path, 'current', integration)
|
|
139
|
+
console.log(`[OAuthIntegrationDatabaseService] Saved ${provider} integration for user ${userId}`)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`[OAuthIntegrationDatabaseService] Failed to save ${provider} integration:`, error)
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static async getUserIntegrations(userId: string, providers: string[]): Promise<Record<string, OAuthIntegration>> {
|
|
147
|
+
const integrations: Record<string, OAuthIntegration> = {}
|
|
148
|
+
|
|
149
|
+
await Promise.all(
|
|
150
|
+
providers.map(async (provider) => {
|
|
151
|
+
const integration = await this.getIntegration(userId, provider)
|
|
152
|
+
if (integration && integration.connected) {
|
|
153
|
+
integrations[provider] = integration
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return integrations
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### API Service (Client-Side)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// src/services/integrations.service.ts
|
|
167
|
+
import type { OAuthIntegration } from '@/schemas/oauth-integration'
|
|
168
|
+
|
|
169
|
+
export class IntegrationsService {
|
|
170
|
+
/**
|
|
171
|
+
* Get user's OAuth integrations (client-side)
|
|
172
|
+
* Calls the API endpoint which validates session server-side
|
|
173
|
+
*/
|
|
174
|
+
static async getUserIntegrations(): Promise<Record<string, OAuthIntegration>> {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch('/api/integrations/')
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error(`Failed to fetch integrations: ${response.statusText}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const data: any = await response.json()
|
|
183
|
+
return data.integrations || {}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('[IntegrationsService] Failed to fetch integrations:', error)
|
|
186
|
+
return {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Examples
|
|
195
|
+
|
|
196
|
+
### Example 1: Using API Service in Component (Client-Side)
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// src/components/integrations/IntegrationsPanel.tsx
|
|
200
|
+
import { useEffect, useState } from 'react'
|
|
201
|
+
import { IntegrationsService } from '@/services/integrations.service'
|
|
202
|
+
import type { OAuthIntegration } from '@/schemas/oauth-integration'
|
|
203
|
+
|
|
204
|
+
function IntegrationsPanel() {
|
|
205
|
+
const [integrations, setIntegrations] = useState<Record<string, OAuthIntegration>>({})
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
// ✅ CORRECT: Use API service wrapper
|
|
209
|
+
IntegrationsService.getUserIntegrations()
|
|
210
|
+
.then(data => setIntegrations(data))
|
|
211
|
+
}, [])
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div>
|
|
215
|
+
{Object.entries(integrations).map(([provider, integration]) => (
|
|
216
|
+
<div key={provider}>
|
|
217
|
+
{provider}: {integration.connected ? 'Connected' : 'Disconnected'}
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Example 2: Using Database Service in API Route (Server-Side)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// src/routes/api/integrations/index.ts
|
|
229
|
+
import { createAPIFileRoute } from '@tanstack/start/api'
|
|
230
|
+
import { getServerSession } from '@/lib/auth/session'
|
|
231
|
+
import { OAuthIntegrationDatabaseService } from '@/services/oauth-integration-database.service'
|
|
232
|
+
|
|
233
|
+
export const APIRoute = createAPIFileRoute('/api/integrations')({
|
|
234
|
+
GET: async ({ request }) => {
|
|
235
|
+
const session = await getServerSession(request)
|
|
236
|
+
|
|
237
|
+
if (!session?.user) {
|
|
238
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ✅ CORRECT: Use database service
|
|
242
|
+
const integrations = await OAuthIntegrationDatabaseService.getUserIntegrations(
|
|
243
|
+
session.user.uid,
|
|
244
|
+
['instagram', 'eventbrite']
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return Response.json({ integrations })
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Example 3: Using Database Service in beforeLoad (Server-Side)
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// src/routes/integrations.tsx
|
|
256
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
257
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
258
|
+
import { OAuthIntegrationDatabaseService } from '@/services/oauth-integration-database.service'
|
|
259
|
+
|
|
260
|
+
export const Route = createFileRoute('/integrations')({
|
|
261
|
+
beforeLoad: async () => {
|
|
262
|
+
const user = await getAuthSession()
|
|
263
|
+
if (!user) return { initialIntegrations: {} }
|
|
264
|
+
|
|
265
|
+
// ✅ CORRECT: Use database service in server-side context
|
|
266
|
+
const initialIntegrations = await OAuthIntegrationDatabaseService.getUserIntegrations(
|
|
267
|
+
user.uid,
|
|
268
|
+
['instagram', 'eventbrite']
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return { initialIntegrations }
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Benefits
|
|
279
|
+
|
|
280
|
+
### 1. Testability
|
|
281
|
+
|
|
282
|
+
Services can be easily mocked for testing without requiring database connections or external services:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Easy to mock services in tests
|
|
286
|
+
jest.mock('@/services/integrations.service')
|
|
287
|
+
|
|
288
|
+
test('component loads integrations', async () => {
|
|
289
|
+
IntegrationsService.getUserIntegrations.mockResolvedValue({ instagram: {...} })
|
|
290
|
+
// Test component behavior
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 2. Consistency
|
|
295
|
+
|
|
296
|
+
All Firestore operations follow the same pattern with consistent error handling, logging, and Zod validation. Changes to database structure or API endpoints only need to be updated in one place.
|
|
297
|
+
|
|
298
|
+
### 3. Type Safety
|
|
299
|
+
|
|
300
|
+
Services provide typed interfaces with no `any` types leaking into components. Zod validation at service boundaries ensures data integrity.
|
|
301
|
+
|
|
302
|
+
### 4. Maintainability
|
|
303
|
+
|
|
304
|
+
Change database structure in one place, update API endpoints in one place, and easily add caching, retry logic, or other cross-cutting concerns.
|
|
305
|
+
|
|
306
|
+
### 5. Import Errors Prevent Misuse
|
|
307
|
+
|
|
308
|
+
Can't accidentally use database service in component - TypeScript import errors will prevent it since database services use server-only imports.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Trade-offs
|
|
313
|
+
|
|
314
|
+
### 1. Additional Complexity
|
|
315
|
+
|
|
316
|
+
**Downside**: Adds extra layers and files to the codebase, which can feel like over-engineering for simple applications.
|
|
317
|
+
|
|
318
|
+
**Mitigation**: Only apply this pattern when complexity justifies it. For very simple CRUD apps, direct database access might be acceptable. Start simple and refactor to this pattern as needs grow.
|
|
319
|
+
|
|
320
|
+
### 2. Boilerplate Code
|
|
321
|
+
|
|
322
|
+
**Downside**: Requires creating multiple service files and maintaining parallel API/Database service structures.
|
|
323
|
+
|
|
324
|
+
**Mitigation**: Use code generation or templates to quickly scaffold new services. The consistency benefits outweigh the initial setup cost.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Anti-Patterns
|
|
329
|
+
|
|
330
|
+
### ❌ Anti-Pattern 1: Direct Firestore Calls in Components
|
|
331
|
+
|
|
332
|
+
**Description**: Calling `setDocument`, `getDocument`, or `queryDocuments` directly from React components.
|
|
333
|
+
|
|
334
|
+
**Why it's bad**: Violates separation of concerns, makes testing difficult, couples components to database implementation, can't be used client-side safely.
|
|
335
|
+
|
|
336
|
+
**Instead, do this**: Use API service wrappers that call API endpoints.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// ❌ BAD: Direct Firestore call in component
|
|
340
|
+
import { setDocument } from '@prmichaelsen/firebase-admin-sdk-v8'
|
|
341
|
+
|
|
342
|
+
function MyComponent() {
|
|
343
|
+
const handleSave = async () => {
|
|
344
|
+
await setDocument('users', userId, data) // BAD!
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ✅ GOOD: Use service layer
|
|
349
|
+
import { UserService } from '@/services/user.service'
|
|
350
|
+
|
|
351
|
+
function MyComponent() {
|
|
352
|
+
const handleSave = async () => {
|
|
353
|
+
await UserService.updateUser(userId, data) // GOOD!
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### ❌ Anti-Pattern 2: Direct fetch Calls in Components
|
|
359
|
+
|
|
360
|
+
**Description**: Calling `fetch('/api/...')` directly from components instead of using service wrappers.
|
|
361
|
+
|
|
362
|
+
**Why it's bad**: Scatters API endpoint knowledge throughout codebase, makes refactoring difficult, no centralized error handling.
|
|
363
|
+
|
|
364
|
+
**Instead, do this**: Create API service wrappers.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// ❌ BAD: Direct fetch in component
|
|
368
|
+
function MyComponent() {
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
fetch('/api/integrations') // BAD!
|
|
371
|
+
.then(res => res.json())
|
|
372
|
+
.then(data => setData(data))
|
|
373
|
+
}, [])
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ✅ GOOD: Use service layer
|
|
377
|
+
import { IntegrationsService } from '@/services/integrations.service'
|
|
378
|
+
|
|
379
|
+
function MyComponent() {
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
IntegrationsService.getUserIntegrations() // GOOD!
|
|
382
|
+
.then(integrations => setData(integrations))
|
|
383
|
+
}, [])
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### ❌ Anti-Pattern 3: Mixing UI Logic in Services
|
|
388
|
+
|
|
389
|
+
**Description**: Putting UI concerns (toasts, navigation, etc.) inside service methods.
|
|
390
|
+
|
|
391
|
+
**Why it's bad**: Services should be pure data operations. UI logic belongs in components.
|
|
392
|
+
|
|
393
|
+
**Instead, do this**: Return data from services and handle UI in components.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// ❌ BAD: Service doing UI logic
|
|
397
|
+
export class UserDatabaseService {
|
|
398
|
+
static async saveUser(user: User): Promise<void> {
|
|
399
|
+
await setDocument(...)
|
|
400
|
+
toast.success('User saved!') // UI logic in service!
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ✅ GOOD: Service returns data, component handles UI
|
|
405
|
+
export class UserDatabaseService {
|
|
406
|
+
static async saveUser(user: User): Promise<void> {
|
|
407
|
+
await setDocument(...)
|
|
408
|
+
// No UI logic
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Component handles UI
|
|
413
|
+
function MyComponent() {
|
|
414
|
+
const handleSave = async () => {
|
|
415
|
+
await UserDatabaseService.saveUser(user)
|
|
416
|
+
toast.success('User saved!') // UI logic in component
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Testing Strategy
|
|
424
|
+
|
|
425
|
+
### Unit Testing Services
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Test database service with mocked Firestore
|
|
429
|
+
jest.mock('@prmichaelsen/firebase-admin-sdk-v8')
|
|
430
|
+
|
|
431
|
+
describe('OAuthIntegrationDatabaseService', () => {
|
|
432
|
+
it('should get integration', async () => {
|
|
433
|
+
const mockDoc = { connected: true, provider: 'instagram' }
|
|
434
|
+
getDocument.mockResolvedValue(mockDoc)
|
|
435
|
+
|
|
436
|
+
const result = await OAuthIntegrationDatabaseService.getIntegration('user1', 'instagram')
|
|
437
|
+
|
|
438
|
+
expect(result).toEqual(mockDoc)
|
|
439
|
+
expect(getDocument).toHaveBeenCalledWith(
|
|
440
|
+
expect.stringContaining('user1'),
|
|
441
|
+
'current'
|
|
442
|
+
)
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Integration Testing Components
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// Test component with mocked API service
|
|
451
|
+
jest.mock('@/services/integrations.service')
|
|
452
|
+
|
|
453
|
+
describe('IntegrationsPanel', () => {
|
|
454
|
+
it('should load integrations', async () => {
|
|
455
|
+
IntegrationsService.getUserIntegrations.mockResolvedValue({
|
|
456
|
+
instagram: { connected: true }
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
render(<IntegrationsPanel />)
|
|
460
|
+
|
|
461
|
+
await waitFor(() => {
|
|
462
|
+
expect(screen.getByText(/instagram/i)).toBeInTheDocument()
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Related Patterns
|
|
471
|
+
|
|
472
|
+
- **[User-Scoped Collections](./tanstack-cloudflare.user-scoped-collections.md)**: Database services use user-scoped collection paths for data isolation
|
|
473
|
+
- **[SSR Preload Pattern](./tanstack-cloudflare.ssr-preload.md)**: Database services are used in `beforeLoad` for server-side data preloading
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Migration Guide
|
|
478
|
+
|
|
479
|
+
### Step 1: Identify Direct Calls
|
|
480
|
+
|
|
481
|
+
Search codebase for:
|
|
482
|
+
- `setDocument(`
|
|
483
|
+
- `getDocument(`
|
|
484
|
+
- `queryDocuments(`
|
|
485
|
+
- `fetch('/api/`
|
|
486
|
+
|
|
487
|
+
### Step 2: Create Services
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// src/services/domain-database.service.ts
|
|
491
|
+
export class DomainDatabaseService {
|
|
492
|
+
static async operation(): Promise<Result> {
|
|
493
|
+
// Move database logic here
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/services/domain.service.ts
|
|
498
|
+
export class DomainService {
|
|
499
|
+
static async operation(): Promise<Result> {
|
|
500
|
+
// Move API logic here
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Step 3: Update Callers
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// Before
|
|
509
|
+
await setDocument(path, id, data)
|
|
510
|
+
|
|
511
|
+
// After (in API route)
|
|
512
|
+
await DomainDatabaseService.saveEntity(id, data)
|
|
513
|
+
|
|
514
|
+
// After (in component)
|
|
515
|
+
await DomainService.saveEntity(id, data)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Step 4: Test
|
|
519
|
+
|
|
520
|
+
- Verify functionality unchanged
|
|
521
|
+
- Add unit tests for services
|
|
522
|
+
- Mock services in component tests
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## References
|
|
527
|
+
|
|
528
|
+
- [TanStack Start Documentation](https://tanstack.com/start/latest)
|
|
529
|
+
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
|
530
|
+
- [firebase-admin-sdk-v8](https://github.com/prmichaelsen/firebase-admin-sdk-v8)
|
|
531
|
+
- [Martin Fowler - Service Layer](https://martinfowler.com/eaaCatalog/serviceLayer.html)
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Checklist for Implementation
|
|
536
|
+
|
|
537
|
+
- [ ] Database services use `{Domain}DatabaseService` naming
|
|
538
|
+
- [ ] API services use `{Domain}Service` naming
|
|
539
|
+
- [ ] Same method names across both service types
|
|
540
|
+
- [ ] No direct `getDocument`/`setDocument` calls outside services
|
|
541
|
+
- [ ] No direct `fetch` calls outside services
|
|
542
|
+
- [ ] Services handle Zod validation
|
|
543
|
+
- [ ] Services log errors appropriately
|
|
544
|
+
- [ ] Services return typed data models
|
|
545
|
+
- [ ] Unit tests cover service logic
|
|
546
|
+
- [ ] Components mock services in tests
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
**Status**: Stable - Proven pattern for TanStack Start + Cloudflare applications
|
|
551
|
+
**Recommendation**: Use for all TanStack Start applications with server-side data access
|
|
552
|
+
**Last Updated**: 2026-02-21
|
|
553
|
+
**Contributors**: Patrick Michaelsen
|