mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 13:46:41 +02:00
feat: integrate uload and picture, unify package naming
- Add uload project with apps/web structure
- Reorganize from flat to monorepo structure
- Remove PocketBase binary and local data
- Update to pnpm and @uload/web namespace
- Add picture project to monorepo
- Remove embedded git repository
- Unify all package names to @{project}/{app} schema:
- @maerchenzauber/* (was @storyteller/*)
- @manacore/* (was manacore-*, manacore)
- @manadeck/* (was web, backend, manadeck)
- @memoro/* (was memoro-web, landing, memoro)
- @picture/* (already unified)
- @uload/web
- Add convenient dev scripts for all apps:
- pnpm dev:{project}:web
- pnpm dev:{project}:landing
- pnpm dev:{project}:mobile
- pnpm dev:{project}:backend
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c6c4c5a552
commit
c712a2504a
1031 changed files with 189301 additions and 290 deletions
404
uload/apps/web/src/lib/db/schema.ts
Normal file
404
uload/apps/web/src/lib/db/schema.ts
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
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] })
|
||||
}))
|
||||
Loading…
Add table
Add a link
Reference in a new issue