mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
feat(uload): rewrite to local-first + Hono architecture
- 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>
This commit is contained in:
parent
5f187705e2
commit
d847eb4115
262 changed files with 44750 additions and 2022 deletions
23
apps/uload/packages/uload-database/package.json
Normal file
23
apps/uload/packages/uload-database/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@manacore/uload-database",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"drizzle-orm": ">=0.38.0",
|
||||
"postgres": ">=3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"postgres": "^3.4.7",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
45
apps/uload/packages/uload-database/src/index.ts
Normal file
45
apps/uload/packages/uload-database/src/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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://uload:uload_dev_password_123@localhost:5432/uload_dev';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
186
apps/uload/packages/uload-database/src/schema.ts
Normal file
186
apps/uload/packages/uload-database/src/schema.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
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),
|
||||
}));
|
||||
14
apps/uload/packages/uload-database/tsconfig.json
Normal file
14
apps/uload/packages/uload-database/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue