mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 21:26:41 +02:00
Feat: Landingpages centralized, new app news integrated
This commit is contained in:
parent
36b85fc8a0
commit
865d74ff37
91 changed files with 8242 additions and 610 deletions
15
packages/news-database/drizzle.config.ts
Normal file
15
packages/news-database/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { config } from 'dotenv';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// Load .env from monorepo root
|
||||
config({ path: resolve(__dirname, '../../.env') });
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/schema/index.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || 'postgresql://news:news_dev_password@localhost:5434/news_hub',
|
||||
},
|
||||
});
|
||||
43
packages/news-database/package.json
Normal file
43
packages/news-database/package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "@manacore/news-database",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./schema": {
|
||||
"types": "./dist/schema/index.d.ts",
|
||||
"import": "./dist/schema/index.js",
|
||||
"require": "./dist/schema/index.js",
|
||||
"default": "./dist/schema/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"prepare": "pnpm build",
|
||||
"db:generate": "dotenv -- drizzle-kit generate",
|
||||
"db:migrate": "dotenv -- drizzle-kit migrate",
|
||||
"db:push": "dotenv -- drizzle-kit push --force",
|
||||
"db:studio": "dotenv -- drizzle-kit studio",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.36.0",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-cli": "^7.4.0",
|
||||
"drizzle-kit": "^0.28.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.6.0",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
19
packages/news-database/src/index.ts
Normal file
19
packages/news-database/src/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
|
||||
// Re-export schema and types
|
||||
export * from './schema';
|
||||
export { sql, eq, and, or, desc, asc, isNull, isNotNull, inArray } from 'drizzle-orm';
|
||||
|
||||
// Export schema for use in drizzle initialization
|
||||
export { schema };
|
||||
|
||||
// Type for the database instance with schema
|
||||
export type Database = PostgresJsDatabase<typeof schema>;
|
||||
|
||||
// Helper to create a new database connection
|
||||
export function createDb(url: string): Database {
|
||||
const client = postgres(url);
|
||||
return drizzle(client, { schema });
|
||||
}
|
||||
64
packages/news-database/src/schema/articles.ts
Normal file
64
packages/news-database/src/schema/articles.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { pgTable, uuid, text, timestamp, boolean, integer, real, pgEnum, index } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
import { categories } from './categories';
|
||||
|
||||
export const articleTypeEnum = pgEnum('article_type', ['feed', 'summary', 'in_depth', 'saved']);
|
||||
export const articleSourceEnum = pgEnum('article_source', ['ai', 'user_saved']);
|
||||
export const summaryPeriodEnum = pgEnum('summary_period', ['morning', 'noon', 'evening', 'night']);
|
||||
|
||||
export const articles = pgTable('articles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// Core fields
|
||||
type: articleTypeEnum('type').notNull(),
|
||||
sourceOrigin: articleSourceEnum('source_origin').default('ai').notNull(),
|
||||
title: text('title').notNull(),
|
||||
content: text('content').notNull(),
|
||||
summary: text('summary'),
|
||||
|
||||
// For user-saved articles
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
originalUrl: text('original_url'),
|
||||
parsedContent: text('parsed_content'),
|
||||
isArchived: boolean('is_archived').default(false),
|
||||
|
||||
// Metadata
|
||||
categoryId: uuid('category_id').references(() => categories.id),
|
||||
sourceUrl: text('source_url'),
|
||||
sourceName: text('source_name'),
|
||||
sourceDomain: text('source_domain'),
|
||||
author: text('author'),
|
||||
imageUrl: text('image_url'),
|
||||
|
||||
// AI-generated metadata
|
||||
aiTags: text('ai_tags').array(),
|
||||
sentimentScore: real('sentiment_score'),
|
||||
|
||||
// Reading metrics
|
||||
readingTimeMinutes: integer('reading_time_minutes'),
|
||||
wordCount: integer('word_count'),
|
||||
|
||||
// Summary-specific fields
|
||||
summaryDate: timestamp('summary_date'),
|
||||
summaryPeriod: summaryPeriodEnum('summary_period'),
|
||||
includedArticleIds: uuid('included_article_ids').array(),
|
||||
|
||||
// In-depth specific fields
|
||||
keyInsights: text('key_insights'), // JSON string
|
||||
dataVisualizations: text('data_visualizations'), // JSON string
|
||||
relatedArticleIds: uuid('related_article_ids').array(),
|
||||
|
||||
// Timestamps
|
||||
publishedAt: timestamp('published_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
index('articles_type_idx').on(table.type),
|
||||
index('articles_user_idx').on(table.userId),
|
||||
index('articles_source_origin_idx').on(table.sourceOrigin),
|
||||
index('articles_published_at_idx').on(table.publishedAt),
|
||||
index('articles_category_idx').on(table.categoryId),
|
||||
]);
|
||||
|
||||
export type Article = typeof articles.$inferSelect;
|
||||
export type NewArticle = typeof articles.$inferInsert;
|
||||
47
packages/news-database/src/schema/auth.ts
Normal file
47
packages/news-database/src/schema/auth.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
|
||||
// Better Auth Sessions
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Better Auth Accounts (for OAuth providers)
|
||||
export const accounts = pgTable('accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
providerId: text('provider_id').notNull(), // 'credential', 'google', 'apple', etc.
|
||||
accountId: text('account_id').notNull(), // Provider's user ID or email for credential
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
||||
scope: text('scope'),
|
||||
password: text('password'), // Hashed, only for credential provider
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Better Auth Verification Tokens
|
||||
export const verifications = pgTable('verifications', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
identifier: text('identifier').notNull(), // email or other identifier
|
||||
value: text('value').notNull(), // the token
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type Session = typeof sessions.$inferSelect;
|
||||
export type NewSession = typeof sessions.$inferInsert;
|
||||
export type Account = typeof accounts.$inferSelect;
|
||||
export type NewAccount = typeof accounts.$inferInsert;
|
||||
export type Verification = typeof verifications.$inferSelect;
|
||||
export type NewVerification = typeof verifications.$inferInsert;
|
||||
16
packages/news-database/src/schema/categories.ts
Normal file
16
packages/news-database/src/schema/categories.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { pgTable, uuid, text, timestamp, integer } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const categories = pgTable('categories', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull().unique(),
|
||||
displayName: text('display_name').notNull(),
|
||||
description: text('description'),
|
||||
icon: text('icon'),
|
||||
color: text('color'),
|
||||
priority: integer('priority').default(0).notNull(),
|
||||
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type Category = typeof categories.$inferSelect;
|
||||
export type NewCategory = typeof categories.$inferInsert;
|
||||
5
packages/news-database/src/schema/index.ts
Normal file
5
packages/news-database/src/schema/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from './users';
|
||||
export * from './categories';
|
||||
export * from './articles';
|
||||
export * from './interactions';
|
||||
export * from './auth';
|
||||
31
packages/news-database/src/schema/interactions.ts
Normal file
31
packages/news-database/src/schema/interactions.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { pgTable, uuid, timestamp, boolean, real, integer, index, unique } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
import { articles } from './articles';
|
||||
|
||||
export const userArticleInteractions = pgTable('user_article_interactions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
articleId: uuid('article_id').references(() => articles.id, { onDelete: 'cascade' }).notNull(),
|
||||
|
||||
// Interaction states
|
||||
isRead: boolean('is_read').default(false).notNull(),
|
||||
isSaved: boolean('is_saved').default(false).notNull(),
|
||||
readProgress: real('read_progress').default(0), // 0.0 to 1.0
|
||||
rating: integer('rating'), // 1-5
|
||||
shareCount: integer('share_count').default(0).notNull(),
|
||||
|
||||
// Timestamps
|
||||
openedAt: timestamp('opened_at'),
|
||||
readAt: timestamp('read_at'),
|
||||
savedAt: timestamp('saved_at'),
|
||||
ratedAt: timestamp('rated_at'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
unique('user_article_unique').on(table.userId, table.articleId),
|
||||
index('interactions_user_idx').on(table.userId),
|
||||
index('interactions_article_idx').on(table.articleId),
|
||||
]);
|
||||
|
||||
export type UserArticleInteraction = typeof userArticleInteractions.$inferSelect;
|
||||
export type NewUserArticleInteraction = typeof userArticleInteractions.$inferInsert;
|
||||
29
packages/news-database/src/schema/users.ts
Normal file
29
packages/news-database/src/schema/users.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { pgTable, uuid, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const userTierEnum = pgEnum('user_tier', ['free', 'premium', 'enterprise']);
|
||||
export const readingSpeedEnum = pgEnum('reading_speed', ['slow', 'normal', 'fast']);
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
email: text('email').notNull().unique(),
|
||||
name: text('name'),
|
||||
avatarUrl: text('avatar_url'),
|
||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||
|
||||
// Preferences
|
||||
tier: userTierEnum('tier').default('free').notNull(),
|
||||
readingSpeed: readingSpeedEnum('reading_speed').default('normal').notNull(),
|
||||
preferredCategories: text('preferred_categories').array(),
|
||||
blockedSources: text('blocked_sources').array(),
|
||||
|
||||
// Settings
|
||||
onboardingCompleted: boolean('onboarding_completed').default(false).notNull(),
|
||||
notificationSettings: text('notification_settings'), // JSON string
|
||||
|
||||
// Timestamps
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type NewUser = typeof users.$inferInsert;
|
||||
21
packages/news-database/tsconfig.json
Normal file
21
packages/news-database/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue