Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 85 additions & 25 deletions .claude/skills/data-layer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,87 @@ description: This skill provides patterns for working with the data-layer module
## Architecture

```
src/data-layer/ # Isolated, framework-agnostic module
├── api/ # Fetch functions (one per data source)
├── index.ts # Getter functions (pure passthrough)
└── registry.ts # Task registry (hourly/daily)
src/data-layer/
├── fetchers/ # Fetch functions (one per data source)
├── index.ts # Public API - typed getter functions
├── tasks.ts # KEYS constant + Trigger.dev scheduled tasks
├── storage.ts # get/set abstraction (Netlify Blobs or mock files)
└── mocks/ # Mock data files for local development

src/lib/data/
└── index.ts # Next.js caching adapter (createCachedGetter)
```

## Key Files

### tasks.ts - Single Source of Truth

Defines all task keys and scheduled jobs:

```typescript
export const KEYS = {
ETH_PRICE: "fetch-eth-price",
L2BEAT: "fetch-l2beat",
// ...
} as const

const DAILY: Task[] = [
[KEYS.APPS, fetchApps],
[KEYS.EVENTS, fetchEvents],
]

const HOURLY: Task[] = [
[KEYS.ETH_PRICE, fetchEthPrice],
[KEYS.BEACONCHAIN_EPOCH, fetchBeaconChainEpoch],
]
```

src/lib/data/ # Next.js caching adapter
└── index.ts # Cached wrappers via createCachedGetter()
### index.ts - Simple Getters

One-liner passthrough functions:

```typescript
export const getEthPrice = () => get<MetricReturnData>(KEYS.ETH_PRICE)
export const getL2beatData = () => get<L2beatData>(KEYS.L2BEAT)
```

### storage.ts - Storage Abstraction

Simple get/set that switches between Netlify Blobs (prod) and local JSON files (dev):

```typescript
export async function get<T>(key: string): Promise<T | null>
export async function set(key: string, data: unknown): Promise<void>
```

Uses `USE_MOCK_DATA=true` env var for local development.

## Rules

### 1. Getters must be pure passthrough

In `src/data-layer/index.ts`, getter functions must only call `getData<T>(TASK_ID)` with no transformations:
No transformations in `index.ts` - just `get<T>(KEYS.X)`:

```typescript
// Correct
export async function getEventsData(): Promise<EventItem[] | null> {
return getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
}
export const getEventsData = () => get<EventItem[]>(KEYS.EVENTS)

// Wrong - no transformations in getters
export async function getEventsData(): Promise<EventItem[] | null> {
const data = await getData<EventItem[]>(FETCH_EVENTS_TASK_ID)
return data?.map((e) => ({ ...e, computed: derive(e) })) ?? null
export const getEventsData = () => {
const data = await get<EventItem[]>(KEYS.EVENTS)
return data?.map(transform) ?? null
}
```

All transformations belong in the fetch task (`src/data-layer/api/`), not in the getter.
All transformations belong in the fetcher (`src/data-layer/fetchers/`).

### 2. KEYS is the single source of truth

### 2. Expose via lib/data for caching
All task IDs are defined in `KEYS` in `tasks.ts`. The getter in `index.ts` and the task tuple in `DAILY`/`HOURLY` must use the same key.

When a data function needs caching/revalidation, add a cached wrapper in `src/lib/data/index.ts`:
### 3. Expose via lib/data for caching

Add cached wrapper in `src/lib/data/index.ts`:

```typescript
export const getEventsData = createCachedGetter(
Expand All @@ -52,25 +100,37 @@ export const getEventsData = createCachedGetter(

## Adding a New Data Source

1. Create fetch function in `src/data-layer/api/fetchNewData.ts`:
1. **Create fetcher** in `src/data-layer/fetchers/fetchNewData.ts`:
```typescript
export const FETCH_NEW_DATA_TASK_ID = "fetch-new-data"

export async function fetchNewData(): Promise<YourDataType> {
// Fetch and transform data here
}
```

2. Add getter in `src/data-layer/index.ts`:
2. **Add key** to `KEYS` in `src/data-layer/tasks.ts`:
```typescript
export async function getNewData(): Promise<YourDataType | null> {
return getData<YourDataType>(FETCH_NEW_DATA_TASK_ID)
}
export const KEYS = {
// ...existing keys
NEW_DATA: "fetch-new-data",
} as const
```

3. **Add task tuple** to `DAILY` or `HOURLY` in `tasks.ts`:
```typescript
const DAILY: Task[] = [
// ...existing tasks
[KEYS.NEW_DATA, fetchNewData],
]
```

4. **Add getter** in `src/data-layer/index.ts`:
```typescript
export const getNewData = () => get<YourDataType>(KEYS.NEW_DATA)
```

3. Register in `src/data-layer/registry.ts` (hourlyTasks or dailyTasks)
5. **Add mock file** at `src/data-layer/mocks/fetch-new-data.json` for local development

4. Add cached wrapper in `src/lib/data/index.ts`:
6. **Add cached wrapper** in `src/lib/data/index.ts`:
```typescript
export const getNewData = createCachedGetter(
dataLayer.getNewData,
Expand Down
10 changes: 8 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": [
"prettier-plugin-tailwindcss"
"plugins": ["prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "src/data-layer/index.ts",
"options": {
"printWidth": 120
}
}
]
}
2 changes: 1 addition & 1 deletion app/[locale]/community/events/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { parseLocationToContinent } from "@/lib/utils/geography"
import { slugify } from "@/lib/utils/url"

import communityMeetups from "@/data/community-meetups.json"
import { getEventTypes } from "@/data-layer/api/fetchEvents"
import { getEventTypes } from "@/data-layer/fetchers/fetchEvents"

// Map EventType to Tag component status colors
export const TAG_STATUS_MAPPING: Record<EventType, TagProps["status"]> = {
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"test:e2e:ui": "playwright test --project=e2e --ui",
"test:e2e:debug": "playwright test --project=e2e --debug",
"test:e2e:report": "playwright show-report tests/__report__",
"trigger:dev": "dotenv -e .env -- npx trigger.dev@latest dev",
"trigger:deploy": "dotenv -e .env -- npx trigger.dev@latest deploy"
"trigger:dev": "npx trigger.dev@latest dev --env-file .env.local",
"trigger:deploy": "npx trigger.dev@latest deploy --env-file .env.local"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.859.0",
Expand Down Expand Up @@ -140,7 +140,6 @@
"chromatic": "12.0.0",
"decompress": "^4.2.1",
"dotenv": "^16.5.0",
"dotenv-cli": "^11.0.0",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.2",
"eslint-config-prettier": "^9",
Expand Down
28 changes: 0 additions & 28 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading