mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 03:59:40 +02:00
- Move from apps-archived/ to apps/ - Delete NestJS backend, PocketBase, old scripts and docs - Create Hono/Bun redirect server (click tracking, analytics API) - Create @manacore/uload-database shared Drizzle schema package - Add local-first store (Dexie.js) for links, tags, folders - Rewrite Links and Tags pages to use IndexedDB - Simplify hooks, layouts, remove all server-side data loading - Add dev scripts: dev:uload:web, dev:uload:server, dev:uload:local Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
186 lines
6.1 KiB
TypeScript
186 lines
6.1 KiB
TypeScript
import {
|
|
pgTable,
|
|
uuid,
|
|
text,
|
|
boolean,
|
|
integer,
|
|
timestamp,
|
|
jsonb,
|
|
index,
|
|
} from 'drizzle-orm/pg-core';
|
|
import { relations } from 'drizzle-orm';
|
|
|
|
// ============================================
|
|
// Users Table
|
|
// ============================================
|
|
export const users = pgTable(
|
|
'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 = pgTable(
|
|
'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 = pgTable(
|
|
'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 = pgTable(
|
|
'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
|
|
// ============================================
|
|
export const clicks = pgTable(
|
|
'clicks',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
linkId: uuid('link_id')
|
|
.references(() => links.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
ipHash: text('ip_hash'),
|
|
userAgent: text('user_agent'),
|
|
referer: text('referer'),
|
|
browser: text('browser'),
|
|
deviceType: text('device_type'),
|
|
os: text('os'),
|
|
country: text('country'),
|
|
city: text('city'),
|
|
clickedAt: timestamp('clicked_at').defaultNow().notNull(),
|
|
utmSource: text('utm_source'),
|
|
utmMedium: text('utm_medium'),
|
|
utmCampaign: text('utm_campaign'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
},
|
|
(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),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// 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),
|
|
}));
|