From 475ed87a41757515be25ecd9b510f992a5bd7a57 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 16:23:33 +0200 Subject: [PATCH] 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) --- CLAUDE.md | 2 +- .../packages/uload-database/src/index.ts | 43 +---- .../packages/uload-database/src/schema.ts | 172 ++---------------- 3 files changed, 15 insertions(+), 202 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6775ec5c6..091f285ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/apps/uload/packages/uload-database/src/index.ts b/apps/uload/packages/uload-database/src/index.ts index 28c22705e..3330f5a15 100644 --- a/apps/uload/packages/uload-database/src/index.ts +++ b/apps/uload/packages/uload-database/src/index.ts @@ -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; - -// 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 | null = null; - -export function getDb(): ReturnType> { - 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 { - if (client) { - await client.end(); - client = null; - db = null; - } -} +export type { clicks as ClicksTable } from './schema'; diff --git a/apps/uload/packages/uload-database/src/schema.ts b/apps/uload/packages/uload-database/src/schema.ts index 2a454befd..3f8bf669e 100644 --- a/apps/uload/packages/uload-database/src/schema.ts +++ b/apps/uload/packages/uload-database/src/schema.ts @@ -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(), - 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), -}));