mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 00:59:40 +02:00
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
413 lines
14 KiB
TypeScript
413 lines
14 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(), // For external auth provider
|
|
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 (Business/Team Accounts)
|
|
// ============================================
|
|
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'), // hashed
|
|
maxClicks: integer('max_clicks'),
|
|
expiresAt: timestamp('expires_at'),
|
|
clickCount: integer('click_count').default(0),
|
|
qrCodeUrl: text('qr_code_url'), // File Storage 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 (Analytics)
|
|
// ============================================
|
|
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),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Tags Table
|
|
// ============================================
|
|
export const tags = pgTable(
|
|
'tags',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
color: text('color'),
|
|
icon: text('icon'),
|
|
isPublic: boolean('is_public').default(false),
|
|
usageCount: integer('usage_count').default(0),
|
|
userId: uuid('user_id').references(() => users.id),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdIdx: index('tags_user_id_idx').on(table.userId),
|
|
slugIdx: index('tags_slug_idx').on(table.slug),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Link-Tags Junction Table
|
|
// ============================================
|
|
export const linkTags = pgTable(
|
|
'link_tags',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
linkId: uuid('link_id')
|
|
.references(() => links.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
tagId: uuid('tag_id')
|
|
.references(() => tags.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
linkIdIdx: index('link_tags_link_id_idx').on(table.linkId),
|
|
tagIdIdx: index('link_tags_tag_id_idx').on(table.tagId),
|
|
uniqueLinkTag: index('link_tags_unique_idx').on(table.linkId, table.tagId),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Notifications Table
|
|
// ============================================
|
|
export const notifications = pgTable(
|
|
'notifications',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
type: text('type').notNull(),
|
|
title: text('title').notNull(),
|
|
message: text('message').notNull(),
|
|
data: jsonb('data'),
|
|
read: boolean('read').default(false),
|
|
actionUrl: text('action_url'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdIdx: index('notifications_user_id_idx').on(table.userId),
|
|
readIdx: index('notifications_read_idx').on(table.read),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Shared Access Table (Team Invitations)
|
|
// ============================================
|
|
export const sharedAccess = pgTable(
|
|
'shared_access',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
owner: uuid('owner')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
userId: uuid('user_id').references(() => users.id),
|
|
permissions: jsonb('permissions'),
|
|
invitationStatus: text('invitation_status', {
|
|
enum: ['pending', 'accepted', 'declined'],
|
|
}).default('pending'),
|
|
acceptedAt: timestamp('accepted_at'),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
ownerIdx: index('shared_access_owner_idx').on(table.owner),
|
|
userIdIdx: index('shared_access_user_id_idx').on(table.userId),
|
|
statusIdx: index('shared_access_status_idx').on(table.invitationStatus),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Pending Invitations Table
|
|
// ============================================
|
|
export const pendingInvitations = pgTable(
|
|
'pending_invitations',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
email: text('email').notNull(),
|
|
token: text('token').unique().notNull(),
|
|
owner: uuid('owner')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
expiresAt: timestamp('expires_at').notNull(),
|
|
acceptedAt: timestamp('accepted_at'),
|
|
acceptedBy: uuid('accepted_by').references(() => users.id),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
emailIdx: index('pending_invitations_email_idx').on(table.email),
|
|
tokenIdx: index('pending_invitations_token_idx').on(table.token),
|
|
ownerIdx: index('pending_invitations_owner_idx').on(table.owner),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Feature Requests Table
|
|
// ============================================
|
|
export const featureRequests = pgTable(
|
|
'feature_requests',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
title: text('title').notNull(),
|
|
description: text('description').notNull(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
status: text('status', {
|
|
enum: ['pending', 'reviewing', 'planned', 'completed', 'rejected'],
|
|
}).default('pending'),
|
|
voteCount: integer('vote_count').default(0),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdIdx: index('feature_requests_user_id_idx').on(table.userId),
|
|
statusIdx: index('feature_requests_status_idx').on(table.status),
|
|
voteCountIdx: index('feature_requests_vote_count_idx').on(table.voteCount),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Feature Votes Table
|
|
// ============================================
|
|
export const featureVotes = pgTable(
|
|
'feature_votes',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
featureRequestId: uuid('feature_request_id')
|
|
.references(() => featureRequests.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
featureRequestIdIdx: index('feature_votes_feature_request_id_idx').on(table.featureRequestId),
|
|
userIdIdx: index('feature_votes_user_id_idx').on(table.userId),
|
|
uniqueVote: index('feature_votes_unique_idx').on(table.featureRequestId, table.userId),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Folders Table (minimal usage, keep for future)
|
|
// ============================================
|
|
export const folders = pgTable(
|
|
'folders',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdIdx: index('folders_user_id_idx').on(table.userId),
|
|
})
|
|
);
|
|
|
|
// ============================================
|
|
// Relations (for Drizzle Relational Queries)
|
|
// ============================================
|
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
links: many(links),
|
|
tags: many(tags),
|
|
notifications: many(notifications),
|
|
ownedAccounts: many(accounts),
|
|
ownedWorkspaces: many(workspaces),
|
|
featureRequests: many(featureRequests),
|
|
featureVotes: many(featureVotes),
|
|
folders: many(folders),
|
|
}));
|
|
|
|
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),
|
|
}));
|
|
|
|
export const clicksRelations = relations(clicks, ({ one }) => ({
|
|
link: one(links, { fields: [clicks.linkId], references: [links.id] }),
|
|
}));
|
|
|
|
export const tagsRelations = relations(tags, ({ one, many }) => ({
|
|
user: one(users, { fields: [tags.userId], references: [users.id] }),
|
|
linkTags: many(linkTags),
|
|
}));
|
|
|
|
export const linkTagsRelations = relations(linkTags, ({ one }) => ({
|
|
link: one(links, { fields: [linkTags.linkId], references: [links.id] }),
|
|
tag: one(tags, { fields: [linkTags.tagId], references: [tags.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),
|
|
}));
|
|
|
|
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|
user: one(users, { fields: [notifications.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const featureRequestsRelations = relations(featureRequests, ({ one, many }) => ({
|
|
user: one(users, { fields: [featureRequests.userId], references: [users.id] }),
|
|
votes: many(featureVotes),
|
|
}));
|
|
|
|
export const featureVotesRelations = relations(featureVotes, ({ one }) => ({
|
|
featureRequest: one(featureRequests, {
|
|
fields: [featureVotes.featureRequestId],
|
|
references: [featureRequests.id],
|
|
}),
|
|
user: one(users, { fields: [featureVotes.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const foldersRelations = relations(folders, ({ one }) => ({
|
|
user: one(users, { fields: [folders.userId], references: [users.id] }),
|
|
}));
|