mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 02:41:25 +02:00
Add agent knowledge files for all modules
This commit is contained in:
parent
11324b5e68
commit
dd06bb2e06
243 changed files with 50805 additions and 175 deletions
511
packages/uload-database/.agent/agent.md
Normal file
511
packages/uload-database/.agent/agent.md
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
# ULoad Database Expert
|
||||
|
||||
## Module: @manacore/uload-database
|
||||
**Path:** `packages/uload-database`
|
||||
**Description:** Drizzle ORM database layer for ULoad, a URL shortener and link management platform. Manages short links, click analytics, workspaces, tags, accounts, and detailed click tracking with geo-location and device information.
|
||||
**Tech Stack:** Drizzle ORM 0.36, PostgreSQL (via postgres.js), TypeScript
|
||||
**Key Dependencies:** drizzle-orm, postgres, drizzle-kit
|
||||
|
||||
## Identity
|
||||
You are the **ULoad Database Expert**. You have deep knowledge of:
|
||||
- URL shortening and link management systems
|
||||
- Click analytics and tracking (geo-location, device, browser, UTM parameters)
|
||||
- Multi-tenant architecture with accounts and workspaces
|
||||
- Link organization with tags and many-to-many relationships
|
||||
- Privacy-conscious analytics (IP hashing, not storing PII)
|
||||
- Custom short codes and QR code generation
|
||||
- Link expiration, password protection, and click limits
|
||||
- Team collaboration features (workspace-based link sharing)
|
||||
|
||||
## Expertise
|
||||
- Drizzle ORM schema design for SaaS platforms
|
||||
- Many-to-many relationships (link-tags junction table)
|
||||
- Multi-level organization (users -> accounts -> workspaces -> links)
|
||||
- Click tracking without compromising user privacy
|
||||
- Composite indexes for analytics queries
|
||||
- Link access control patterns (password, expiration, max clicks)
|
||||
- UTM parameter tracking for marketing analytics
|
||||
|
||||
## Code Structure
|
||||
```
|
||||
packages/uload-database/src/
|
||||
├── schema/
|
||||
│ ├── index.ts # Exports all schemas and relations
|
||||
│ ├── users.ts # User accounts
|
||||
│ ├── accounts.ts # Account entities (free/team/enterprise)
|
||||
│ ├── workspaces.ts # Workspaces (personal/team)
|
||||
│ ├── links.ts # Short links with access controls
|
||||
│ ├── clicks.ts # Click tracking and analytics
|
||||
│ ├── tags.ts # Tags and link-tag junction table
|
||||
│ └── relations.ts # All Drizzle relations definitions
|
||||
├── client.ts # Database client factory & singleton
|
||||
└── index.ts # Main entry point
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### 1. Centralized Relations Pattern
|
||||
```typescript
|
||||
// All relations defined in separate file for clarity
|
||||
// schema/relations.ts
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
links: many(links),
|
||||
tags: many(tags),
|
||||
ownedAccounts: many(accounts),
|
||||
ownedWorkspaces: many(workspaces),
|
||||
}));
|
||||
|
||||
export const linksRelations = relations(links, ({ one, many }) => ({
|
||||
user: one(users, { fields: [links.userId], references: [users.id] }),
|
||||
account: one(accounts, { fields: [links.accountOwner], references: [accounts.id] }),
|
||||
workspace: one(workspaces, { fields: [links.workspaceId], references: [workspaces.id] }),
|
||||
clicks: many(clicks),
|
||||
linkTags: many(linkTags),
|
||||
}));
|
||||
```
|
||||
|
||||
### 2. Multi-Level Hierarchy
|
||||
```typescript
|
||||
// User -> Account -> Workspace -> Link
|
||||
User (owner)
|
||||
├─ Account (free/team/enterprise)
|
||||
│ └─ Links (account-owned)
|
||||
└─ Workspace (personal/team)
|
||||
└─ Links (workspace-scoped)
|
||||
```
|
||||
|
||||
### 3. Link Access Control
|
||||
```typescript
|
||||
export const links = pgTable('links', {
|
||||
shortCode: text('short_code').unique().notNull(),
|
||||
customCode: text('custom_code'),
|
||||
originalUrl: text('original_url').notNull(),
|
||||
isActive: boolean('is_active').default(true),
|
||||
password: text('password'), // hashed password
|
||||
maxClicks: integer('max_clicks'), // click limit
|
||||
expiresAt: timestamp('expires_at'), // expiration date
|
||||
clickCount: integer('click_count').default(0),
|
||||
});
|
||||
|
||||
// Access validation logic:
|
||||
// 1. Check isActive
|
||||
// 2. Check expiresAt (if set)
|
||||
// 3. Check maxClicks vs clickCount (if set)
|
||||
// 4. Verify password (if set)
|
||||
```
|
||||
|
||||
### 4. Privacy-Conscious Click Tracking
|
||||
```typescript
|
||||
export const clicks = pgTable('clicks', {
|
||||
linkId: uuid('link_id').notNull(),
|
||||
ipHash: text('ip_hash'), // Hashed, not raw IP
|
||||
userAgent: text('user_agent'), // For analytics
|
||||
browser: text('browser'), // Parsed from UA
|
||||
deviceType: text('device_type'), // mobile/desktop/tablet
|
||||
os: text('os'), // Operating system
|
||||
country: text('country'), // Geo-location
|
||||
city: text('city'),
|
||||
utmSource: text('utm_source'), // Marketing parameters
|
||||
utmMedium: text('utm_medium'),
|
||||
utmCampaign: text('utm_campaign'),
|
||||
clickedAt: timestamp('clicked_at'),
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Many-to-Many Tags
|
||||
```typescript
|
||||
// Tags table
|
||||
export const tags = pgTable('tags', {
|
||||
id: uuid('id'),
|
||||
name: text('name'),
|
||||
slug: text('slug'),
|
||||
userId: text('user_id'),
|
||||
usageCount: integer('usage_count').default(0),
|
||||
});
|
||||
|
||||
// Junction table
|
||||
export const linkTags = pgTable('link_tags', {
|
||||
id: uuid('id'),
|
||||
linkId: uuid('link_id').references(() => links.id, { onDelete: 'cascade' }),
|
||||
tagId: uuid('tag_id').references(() => tags.id, { onDelete: 'cascade' }),
|
||||
});
|
||||
|
||||
// Composite index for uniqueness
|
||||
index('link_tags_unique_idx').on(table.linkId, table.tagId)
|
||||
```
|
||||
|
||||
### 6. Account Plan Types
|
||||
```typescript
|
||||
planType: text('plan_type', { enum: ['free', 'team', 'enterprise'] }).default('free')
|
||||
|
||||
// Plan features:
|
||||
// - free: Single user, basic features
|
||||
// - team: Multiple users, workspace collaboration
|
||||
// - enterprise: Advanced analytics, custom branding
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Used By
|
||||
- ULoad backend (NestJS) - link CRUD and redirection
|
||||
- Analytics service - click tracking and reporting
|
||||
- QR code generation service - visual link sharing
|
||||
- UTM tracking service - marketing campaign analytics
|
||||
|
||||
### Depends On
|
||||
- `drizzle-orm` - ORM and query builder
|
||||
- `postgres` - PostgreSQL client
|
||||
- `drizzle-kit` - Migration tools
|
||||
- External: IP geolocation API, QR code generator
|
||||
|
||||
### Environment Variables
|
||||
- `DATABASE_URL` or `ULOAD_DATABASE_URL` - PostgreSQL connection string
|
||||
|
||||
## Database Schema Overview
|
||||
|
||||
### Core Tables
|
||||
|
||||
1. **users** - User accounts
|
||||
- id (text), email, name, avatarUrl
|
||||
- emailVerified, createdAt, updatedAt
|
||||
|
||||
2. **accounts** - Account entities for billing/plans
|
||||
- id, name, owner (user FK)
|
||||
- isActive, planType (free/team/enterprise)
|
||||
- settings (JSONB) - account-level configuration
|
||||
- createdAt, updatedAt
|
||||
|
||||
3. **workspaces** - Workspaces for organizing links
|
||||
- id, name, slug (unique)
|
||||
- type (personal/team)
|
||||
- owner (user FK)
|
||||
- createdAt, updatedAt
|
||||
|
||||
4. **links** - Short links with full features
|
||||
- **Identification**
|
||||
- shortCode (unique, auto-generated)
|
||||
- customCode (user-defined)
|
||||
- originalUrl
|
||||
|
||||
- **Organization**
|
||||
- userId, accountOwner, workspaceId
|
||||
- title, description
|
||||
|
||||
- **Access Control**
|
||||
- isActive (soft delete)
|
||||
- password (hashed)
|
||||
- maxClicks (limit)
|
||||
- expiresAt (expiration)
|
||||
|
||||
- **Analytics**
|
||||
- clickCount (incremented)
|
||||
- tags (JSONB array)
|
||||
|
||||
- **Marketing**
|
||||
- utmSource, utmMedium, utmCampaign
|
||||
- qrCodeUrl (generated)
|
||||
|
||||
- **Timestamps**
|
||||
- createdAt, updatedAt
|
||||
|
||||
5. **clicks** - Click event tracking
|
||||
- linkId (FK with cascade delete)
|
||||
- **Privacy-Safe Data**
|
||||
- ipHash (SHA256, not raw IP)
|
||||
- userAgent, browser, deviceType, os
|
||||
- country, city (from IP lookup)
|
||||
- referer (referrer URL)
|
||||
- **Marketing Data**
|
||||
- utmSource, utmMedium, utmCampaign
|
||||
- **Timestamps**
|
||||
- clickedAt, createdAt
|
||||
|
||||
6. **tags** - Link tags/labels
|
||||
- name, slug, color, icon
|
||||
- userId (owner)
|
||||
- isPublic (shared tags)
|
||||
- usageCount (popularity)
|
||||
- createdAt, updatedAt
|
||||
|
||||
7. **link_tags** - Many-to-many junction
|
||||
- linkId (FK cascade delete)
|
||||
- tagId (FK cascade delete)
|
||||
- Unique constraint on (linkId, tagId)
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
```bash
|
||||
# Generate migration from schema changes
|
||||
pnpm db:generate
|
||||
|
||||
# Apply migrations to database
|
||||
pnpm db:migrate
|
||||
|
||||
# Push schema directly (dev only, skips migrations)
|
||||
pnpm db:push
|
||||
|
||||
# Open Drizzle Studio for GUI exploration
|
||||
pnpm db:studio
|
||||
|
||||
# Reset database (wipes all data)
|
||||
pnpm db:reset
|
||||
|
||||
# Test connection
|
||||
pnpm db:test
|
||||
```
|
||||
|
||||
## Common Queries
|
||||
|
||||
### Create Short Link
|
||||
```typescript
|
||||
import { getDb, links } from '@manacore/uload-database';
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Generate unique short code (service layer logic)
|
||||
const shortCode = generateShortCode(); // e.g., 'abc123'
|
||||
|
||||
const [link] = await db.insert(links).values({
|
||||
shortCode,
|
||||
originalUrl: 'https://example.com/very/long/url',
|
||||
title: 'My Link',
|
||||
userId: 'user123',
|
||||
workspaceId: workspaceId,
|
||||
}).returning();
|
||||
```
|
||||
|
||||
### Track Click Event
|
||||
```typescript
|
||||
import { clicks, links, eq, sql } from '@manacore/uload-database';
|
||||
import { hashIp } from './utils';
|
||||
|
||||
// 1. Record click
|
||||
await db.insert(clicks).values({
|
||||
linkId: link.id,
|
||||
ipHash: hashIp(request.ip),
|
||||
userAgent: request.headers['user-agent'],
|
||||
browser: parsedUA.browser,
|
||||
deviceType: parsedUA.deviceType,
|
||||
os: parsedUA.os,
|
||||
country: geoData.country,
|
||||
city: geoData.city,
|
||||
referer: request.headers.referer,
|
||||
utmSource: query.utm_source,
|
||||
utmMedium: query.utm_medium,
|
||||
utmCampaign: query.utm_campaign,
|
||||
});
|
||||
|
||||
// 2. Increment click count
|
||||
await db
|
||||
.update(links)
|
||||
.set({ clickCount: sql`${links.clickCount} + 1` })
|
||||
.where(eq(links.id, link.id));
|
||||
```
|
||||
|
||||
### Get Link with Analytics
|
||||
```typescript
|
||||
import { eq, desc } from '@manacore/uload-database';
|
||||
|
||||
// Get link with click history
|
||||
const linkWithClicks = await db.query.links.findFirst({
|
||||
where: eq(links.shortCode, shortCode),
|
||||
with: {
|
||||
clicks: {
|
||||
orderBy: desc(clicks.clickedAt),
|
||||
limit: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get link with tags
|
||||
const linkWithTags = await db.query.links.findFirst({
|
||||
where: eq(links.id, linkId),
|
||||
with: {
|
||||
linkTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Workspace Links
|
||||
```typescript
|
||||
import { eq, and, desc } from '@manacore/uload-database';
|
||||
|
||||
// Get all links in workspace
|
||||
const workspaceLinks = await db
|
||||
.select()
|
||||
.from(links)
|
||||
.where(
|
||||
and(
|
||||
eq(links.workspaceId, workspaceId),
|
||||
eq(links.isActive, true)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(links.createdAt));
|
||||
```
|
||||
|
||||
### Click Analytics
|
||||
```typescript
|
||||
import { sql, eq, gte } from '@manacore/uload-database';
|
||||
|
||||
// Get click stats for last 30 days
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const clickStats = await db
|
||||
.select({
|
||||
totalClicks: sql<number>`COUNT(*)`,
|
||||
uniqueCountries: sql<number>`COUNT(DISTINCT ${clicks.country})`,
|
||||
desktopClicks: sql<number>`COUNT(*) FILTER (WHERE ${clicks.deviceType} = 'desktop')`,
|
||||
mobileClicks: sql<number>`COUNT(*) FILTER (WHERE ${clicks.deviceType} = 'mobile')`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(
|
||||
and(
|
||||
eq(clicks.linkId, linkId),
|
||||
gte(clicks.clickedAt, thirtyDaysAgo)
|
||||
)
|
||||
);
|
||||
|
||||
// Get top referrers
|
||||
const topReferrers = await db
|
||||
.select({
|
||||
referer: clicks.referer,
|
||||
count: sql<number>`COUNT(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId))
|
||||
.groupBy(clicks.referer)
|
||||
.orderBy(desc(sql`COUNT(*)`))
|
||||
.limit(10);
|
||||
|
||||
// Geographic distribution
|
||||
const geoDistribution = await db
|
||||
.select({
|
||||
country: clicks.country,
|
||||
city: clicks.city,
|
||||
count: sql<number>`COUNT(*)`,
|
||||
})
|
||||
.from(clicks)
|
||||
.where(eq(clicks.linkId, linkId))
|
||||
.groupBy(clicks.country, clicks.city)
|
||||
.orderBy(desc(sql`COUNT(*)`));
|
||||
```
|
||||
|
||||
### Tag Operations
|
||||
```typescript
|
||||
import { tags, linkTags, inArray } from '@manacore/uload-database';
|
||||
|
||||
// Create and attach tag to link
|
||||
const [tag] = await db.insert(tags).values({
|
||||
name: 'Marketing',
|
||||
slug: 'marketing',
|
||||
color: '#FF5733',
|
||||
userId: 'user123',
|
||||
}).returning();
|
||||
|
||||
await db.insert(linkTags).values({
|
||||
linkId: link.id,
|
||||
tagId: tag.id,
|
||||
});
|
||||
|
||||
// Get all links with specific tag
|
||||
const linksWithTag = await db.query.tags.findFirst({
|
||||
where: eq(tags.slug, 'marketing'),
|
||||
with: {
|
||||
linkTags: {
|
||||
with: {
|
||||
link: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Increment tag usage count
|
||||
await db
|
||||
.update(tags)
|
||||
.set({ usageCount: sql`${tags.usageCount} + 1` })
|
||||
.where(eq(tags.id, tag.id));
|
||||
```
|
||||
|
||||
### Access Control Validation
|
||||
```typescript
|
||||
import { eq } from '@manacore/uload-database';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
async function validateLinkAccess(
|
||||
shortCode: string,
|
||||
password?: string
|
||||
): Promise<{ valid: boolean; reason?: string }> {
|
||||
const link = await db.query.links.findFirst({
|
||||
where: eq(links.shortCode, shortCode),
|
||||
});
|
||||
|
||||
if (!link) return { valid: false, reason: 'NOT_FOUND' };
|
||||
if (!link.isActive) return { valid: false, reason: 'INACTIVE' };
|
||||
|
||||
// Check expiration
|
||||
if (link.expiresAt && new Date() > link.expiresAt) {
|
||||
return { valid: false, reason: 'EXPIRED' };
|
||||
}
|
||||
|
||||
// Check click limit
|
||||
if (link.maxClicks && link.clickCount >= link.maxClicks) {
|
||||
return { valid: false, reason: 'CLICK_LIMIT_REACHED' };
|
||||
}
|
||||
|
||||
// Check password
|
||||
if (link.password) {
|
||||
if (!password) return { valid: false, reason: 'PASSWORD_REQUIRED' };
|
||||
const isValid = await bcrypt.compare(password, link.password);
|
||||
if (!isValid) return { valid: false, reason: 'INVALID_PASSWORD' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Tenant Patterns
|
||||
|
||||
### Personal Workspace
|
||||
```typescript
|
||||
// Every user gets a personal workspace on signup
|
||||
const [workspace] = await db.insert(workspaces).values({
|
||||
name: `${user.name}'s Workspace`,
|
||||
slug: `${user.id}-personal`,
|
||||
type: 'personal',
|
||||
owner: user.id,
|
||||
}).returning();
|
||||
```
|
||||
|
||||
### Team Workspace
|
||||
```typescript
|
||||
// Team workspaces can have multiple users (via separate membership table)
|
||||
const [teamWorkspace] = await db.insert(workspaces).values({
|
||||
name: 'Marketing Team',
|
||||
slug: 'marketing-team',
|
||||
type: 'team',
|
||||
owner: user.id, // Creator
|
||||
}).returning();
|
||||
|
||||
// Links in team workspace are accessible to all members
|
||||
const teamLinks = await db
|
||||
.select()
|
||||
.from(links)
|
||||
.where(eq(links.workspaceId, teamWorkspace.id));
|
||||
```
|
||||
|
||||
## How to Use
|
||||
```
|
||||
"Read packages/uload-database/.agent/ and help me with..."
|
||||
- Implementing link access control
|
||||
- Optimizing click analytics queries
|
||||
- Understanding multi-tenant architecture
|
||||
- Adding new tag features
|
||||
- Setting up workspace collaboration
|
||||
- Privacy-conscious analytics tracking
|
||||
- Debugging relational queries
|
||||
```
|
||||
6
packages/uload-database/.agent/memory.md
Normal file
6
packages/uload-database/.agent/memory.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# ULoad Database Expert - Memory
|
||||
|
||||
Auto-updated with learnings from code changes.
|
||||
|
||||
## Recent Updates
|
||||
*No updates yet.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue