refactor(uload): remove unused schema tables, keep only clicks

The uload-server reads links from sync_changes (local-first via mana-sync)
and never used the Drizzle schema tables (users, accounts, workspaces, links).
Strip uload-database package to only the clicks table which is needed for
performant analytics aggregation with proper SQL indexes.

- Remove 5 unused tables (users, accounts, workspaces, links, relations)
- Keep only uload.clicks with indexes on link_id, clicked_at, country, device_type
- Simplify uload-database package from ~190 LOC to ~40 LOC

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 16:23:33 +02:00
parent d5c40a4543
commit 475ed87a41
3 changed files with 15 additions and 202 deletions

View file

@ -691,7 +691,7 @@ Each service owns a PostgreSQL schema within the shared database:
| `todo` | todo server | tasks, projects, reminders |
| `traces` | traces server | locations, cities, POIs, guides |
| `presi` | presi server | decks, slides, themes, shares |
| `uload` | uload server | users, links, clicks |
| `uload` | uload server | clicks only (links via sync_changes) |
| `cards` | cards package | decks, cards, study progress |
### Using pgSchema

View file

@ -1,44 +1,3 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
export * from './schema';
// Re-export drizzle operators used by the backend
export { eq, and, or, desc, sql, gte, lte, ilike } from 'drizzle-orm';
// Database instance type
export type Database = ReturnType<typeof getDb>;
// Infer types for backend services
export type Link = typeof schema.links.$inferSelect;
export type NewLink = typeof schema.links.$inferInsert;
export type Click = typeof schema.clicks.$inferSelect;
export type NewClick = typeof schema.clicks.$inferInsert;
let db: Database | null = null;
let client: ReturnType<typeof postgres> | null = null;
export function getDb(): ReturnType<typeof drizzle<typeof schema>> {
if (!db) {
const connectionString =
process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/mana_platform';
client = postgres(connectionString, {
max: 10,
idle_timeout: 20,
connect_timeout: 10,
});
db = drizzle(client, { schema });
}
return db!;
}
export async function closeDb(): Promise<void> {
if (client) {
await client.end();
client = null;
db = null;
}
}
export type { clicks as ClicksTable } from './schema';

View file

@ -1,143 +1,24 @@
import {
pgSchema,
uuid,
text,
boolean,
integer,
timestamp,
jsonb,
index,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
/**
* uLoad database schema minimal server-side tables only.
*
* Links, tags, folders are handled via local-first (IndexedDB mana-sync sync_changes).
* Only clicks need a dedicated table for performant analytics aggregation.
*
* The uload-server reads links from sync_changes and writes clicks here.
*/
import { pgSchema, uuid, text, timestamp, index } from 'drizzle-orm/pg-core';
export const uloadSchema = pgSchema('uload');
// ============================================
// Users Table
// ============================================
export const users = uloadSchema.table(
'users',
{
id: uuid('id').primaryKey().defaultRandom(),
externalAuthId: text('external_auth_id').unique(),
email: text('email').unique().notNull(),
username: text('username').unique().notNull(),
name: text('name'),
avatarUrl: text('avatar_url'),
bio: text('bio'),
location: text('location'),
website: text('website'),
github: text('github'),
twitter: text('twitter'),
linkedin: text('linkedin'),
instagram: text('instagram'),
publicProfile: boolean('public_profile').default(false),
showClickStats: boolean('show_click_stats').default(true),
emailNotifications: boolean('email_notifications').default(true),
defaultExpiry: integer('default_expiry'),
profileBackground: text('profile_background'),
verified: boolean('verified').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
emailIdx: index('users_email_idx').on(table.email),
usernameIdx: index('users_username_idx').on(table.username),
externalAuthIdIdx: index('users_external_auth_id_idx').on(table.externalAuthId),
})
);
// ============================================
// Accounts Table
// ============================================
export const accounts = uloadSchema.table(
'accounts',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
owner: uuid('owner')
.references(() => users.id)
.notNull(),
isActive: boolean('is_active').default(true),
planType: text('plan_type', { enum: ['free', 'team', 'enterprise'] }).default('free'),
settings: jsonb('settings'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
ownerIdx: index('accounts_owner_idx').on(table.owner),
})
);
// ============================================
// Workspaces Table
// ============================================
export const workspaces = uloadSchema.table(
'workspaces',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
slug: text('slug').unique().notNull(),
type: text('type', { enum: ['personal', 'team'] }).notNull(),
owner: uuid('owner')
.references(() => users.id)
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
slugIdx: index('workspaces_slug_idx').on(table.slug),
ownerIdx: index('workspaces_owner_idx').on(table.owner),
})
);
// ============================================
// Links Table
// ============================================
export const links = uloadSchema.table(
'links',
{
id: uuid('id').primaryKey().defaultRandom(),
shortCode: text('short_code').unique().notNull(),
customCode: text('custom_code'),
originalUrl: text('original_url').notNull(),
title: text('title'),
description: text('description'),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
isActive: boolean('is_active').default(true),
password: text('password'),
maxClicks: integer('max_clicks'),
expiresAt: timestamp('expires_at'),
clickCount: integer('click_count').default(0),
qrCodeUrl: text('qr_code_url'),
tags: jsonb('tags').$type<string[]>(),
utmSource: text('utm_source'),
utmMedium: text('utm_medium'),
utmCampaign: text('utm_campaign'),
accountOwner: uuid('account_owner').references(() => accounts.id),
workspaceId: uuid('workspace_id').references(() => workspaces.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
userIdIdx: index('links_user_id_idx').on(table.userId),
shortCodeIdx: index('links_short_code_idx').on(table.shortCode),
workspaceIdIdx: index('links_workspace_id_idx').on(table.workspaceId),
accountOwnerIdx: index('links_account_owner_idx').on(table.accountOwner),
isActiveIdx: index('links_is_active_idx').on(table.isActive),
})
);
// ============================================
// Clicks Table
// Clicks Table — server-generated click tracking
// ============================================
export const clicks = uloadSchema.table(
'clicks',
{
id: uuid('id').primaryKey().defaultRandom(),
linkId: uuid('link_id')
.references(() => links.id, { onDelete: 'cascade' })
.notNull(),
linkId: text('link_id').notNull(),
ipHash: text('ip_hash'),
userAgent: text('user_agent'),
referer: text('referer'),
@ -156,33 +37,6 @@ export const clicks = uloadSchema.table(
linkIdIdx: index('clicks_link_id_idx').on(table.linkId),
clickedAtIdx: index('clicks_clicked_at_idx').on(table.clickedAt),
countryIdx: index('clicks_country_idx').on(table.country),
deviceTypeIdx: index('clicks_device_type_idx').on(table.deviceType),
})
);
// ============================================
// Relations
// ============================================
export const usersRelations = relations(users, ({ many }) => ({
links: many(links),
}));
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),
}));
export const clicksRelations = relations(clicks, ({ one }) => ({
link: one(links, { fields: [clicks.linkId], references: [links.id] }),
}));
export const accountsRelations = relations(accounts, ({ one, many }) => ({
owner: one(users, { fields: [accounts.owner], references: [users.id] }),
links: many(links),
}));
export const workspacesRelations = relations(workspaces, ({ one, many }) => ({
owner: one(users, { fields: [workspaces.owner], references: [users.id] }),
links: many(links),
}));