PRIMEIRO ENVIO

This commit is contained in:
2025-12-25 12:02:07 -03:00
parent ca49575224
commit c4d3bd9a86
92 changed files with 26976 additions and 1 deletions
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/__tests__/**/*'
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testTimeout: 30000,
maxWorkers: 1, // Run tests sequentially for database tests
};
+22
View File
@@ -0,0 +1,22 @@
// Jest setup for database tests
const { execSync } = require('child_process');
// Set test environment variables
process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://cloneweb_user:cloneweb_password@localhost:5432/cloneweb_test';
process.env.NODE_ENV = 'test';
// Global setup - run before all tests
beforeAll(async () => {
// Ensure test database exists and is migrated
try {
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
} catch (error) {
console.warn('Database setup failed, tests may fail:', error.message);
}
});
// Global teardown - run after all tests
afterAll(async () => {
// Clean up test database if needed
// Note: In a real environment, you might want to clean up test data
});
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@cloneweb/database",
"version": "1.0.0",
"description": "Database schemas and migrations",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"migrate": "prisma migrate dev",
"generate": "prisma generate",
"seed": "tsx seed.ts",
"studio": "prisma studio",
"test": "jest",
"test:property": "jest --testNamePattern='Property'",
"test:watch": "jest --watch",
"db:push": "prisma db push",
"db:reset": "prisma db push --force-reset"
},
"dependencies": {
"@prisma/client": "^5.4.2",
"@cloneweb/shared": "file:../shared",
"prisma": "^5.4.2"
},
"devDependencies": {
"@types/jest": "^29.5.5",
"fast-check": "^3.13.2",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"tsx": "^3.14.0",
"typescript": "^5.2.2"
}
}
+291
View File
@@ -0,0 +1,291 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User Management
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
firstName String?
lastName String?
avatar String?
isActive Boolean @default(true)
emailVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Subscription and billing
subscription SubscriptionTier @default(FREE)
billingId String?
usageMetrics UsageMetrics?
// Relationships
projects Project[]
collaborations ProjectCollaboration[]
sessions UserSession[]
auditLogs AuditLog[]
@@map("users")
}
model UserSession {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_sessions")
}
// Project Management
model Project {
id String @id @default(cuid())
name String
description String?
sourceUrl String
status ProjectStatus @default(CREATED)
settings Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Owner
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Relationships
pages Page[]
assets Asset[]
components Component[]
collaborations ProjectCollaboration[]
analytics ProjectAnalytics?
exports ProjectExport[]
@@map("projects")
}
model ProjectCollaboration {
id String @id @default(cuid())
projectId String
userId String
role CollaborationRole @default(VIEWER)
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([projectId, userId])
@@map("project_collaborations")
}
// Site Structure
model Page {
id String @id @default(cuid())
projectId String
url String
title String?
depth Int @default(0)
status PageStatus @default(DISCOVERED)
metadata Json @default("{}")
content String?
screenshot String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
assets Asset[]
@@unique([projectId, url])
@@map("pages")
}
// Asset Management
model Asset {
id String @id @default(cuid())
projectId String
pageId String?
originalUrl String
storedUrl String
filename String
type AssetType
size Int
hash String
optimized Boolean @default(false)
cdnUrl String?
metadata Json @default("{}")
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
page Page? @relation(fields: [pageId], references: [id], onDelete: SetNull)
@@unique([projectId, hash])
@@map("assets")
}
// Component System
model Component {
id String @id @default(cuid())
projectId String
name String
type ComponentType
html String
css String
javascript String?
props Json @default("[]")
frequency Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
instances ComponentInstance[]
@@map("components")
}
model ComponentInstance {
id String @id @default(cuid())
componentId String
selector String
props Json @default("{}")
createdAt DateTime @default(now())
component Component @relation(fields: [componentId], references: [id], onDelete: Cascade)
@@map("component_instances")
}
// Analytics and Metrics
model ProjectAnalytics {
id String @id @default(cuid())
projectId String @unique
cloneAccuracy Float?
processingTime Int? // in milliseconds
totalPages Int @default(0)
totalAssets Int @default(0)
totalComponents Int @default(0)
lastUpdated DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@map("project_analytics")
}
model UsageMetrics {
id String @id @default(cuid())
userId String @unique
pagesCloned Int @default(0)
storageUsed BigInt @default(0) // in bytes
apiCalls Int @default(0)
lastReset DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("usage_metrics")
}
// Export and Deployment
model ProjectExport {
id String @id @default(cuid())
projectId String
format ExportFormat
status ExportStatus @default(PENDING)
fileUrl String?
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@map("project_exports")
}
// Security and Audit
model AuditLog {
id String @id @default(cuid())
userId String?
action String
resource String
details Json @default("{}")
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@map("audit_logs")
}
// Enums
enum SubscriptionTier {
FREE
BASIC
PRO
ENTERPRISE
}
enum ProjectStatus {
CREATED
CRAWLING
PROCESSING
GENERATING
COMPLETED
FAILED
}
enum PageStatus {
DISCOVERED
CRAWLING
SCRAPED
PROCESSED
FAILED
}
enum AssetType {
IMAGE
CSS
JAVASCRIPT
FONT
MEDIA
OTHER
}
enum ComponentType {
NAVIGATION
HEADER
FOOTER
CARD
FORM
BUTTON
OTHER
}
enum CollaborationRole {
VIEWER
EDITOR
ADMIN
}
enum ExportFormat {
ZIP
GITHUB
VERCEL
NETLIFY
}
enum ExportStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
@@ -0,0 +1,144 @@
import fc from 'fast-check';
// Feature: website-cloning-platform, Property 32: PostgreSQL Data Storage
describe('Database Connectivity Properties', () => {
test('Property 32: Database connection configuration should be valid', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
host: fc.domain(),
port: fc.integer({ min: 1024, max: 65535 }),
database: fc.string({ minLength: 1, maxLength: 50 }).filter(s => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)),
username: fc.string({ minLength: 1, maxLength: 50 }).filter(s => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)),
password: fc.string({ minLength: 8, maxLength: 100 }).filter(s => /^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]*$/.test(s) && !s.includes(' '))
}),
async (config) => {
// Test that database URL construction is valid
const encodedPassword = encodeURIComponent(config.password);
const databaseUrl = `postgresql://${config.username}:${encodedPassword}@${config.host}:${config.port}/${config.database}`;
// Validate URL format
try {
const url = new URL(databaseUrl);
return url.protocol === 'postgresql:' &&
url.hostname === config.host &&
url.port === config.port.toString() &&
url.pathname === `/${config.database}` &&
url.username === config.username;
} catch (error) {
return false;
}
}
),
{
numRuns: 100,
timeout: 5000
}
);
});
test('Property 32: Database connection parameters should be properly validated', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
maxConnections: fc.integer({ min: 1, max: 100 }),
connectionTimeout: fc.integer({ min: 1000, max: 60000 }),
queryTimeout: fc.integer({ min: 1000, max: 300000 }),
retryAttempts: fc.integer({ min: 0, max: 10 })
}),
async (params) => {
// Validate that connection parameters are within acceptable ranges
const isValidMaxConnections = params.maxConnections >= 1 && params.maxConnections <= 100;
const isValidConnectionTimeout = params.connectionTimeout >= 1000 && params.connectionTimeout <= 60000;
const isValidQueryTimeout = params.queryTimeout >= 1000 && params.queryTimeout <= 300000;
const isValidRetryAttempts = params.retryAttempts >= 0 && params.retryAttempts <= 10;
return isValidMaxConnections &&
isValidConnectionTimeout &&
isValidQueryTimeout &&
isValidRetryAttempts;
}
),
{
numRuns: 100,
timeout: 5000
}
);
});
test('Property 32: Database schema validation should handle various data types', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
stringField: fc.string({ minLength: 0, maxLength: 1000 }),
integerField: fc.integer({ min: -2147483648, max: 2147483647 }),
booleanField: fc.boolean(),
dateField: fc.date({ min: new Date('1900-01-01'), max: new Date('2100-12-31') }),
optionalField: fc.option(fc.string({ minLength: 1, maxLength: 100 }))
}),
async (data) => {
// Validate that data types are properly handled
const isValidString = typeof data.stringField === 'string';
const isValidInteger = Number.isInteger(data.integerField) &&
data.integerField >= -2147483648 &&
data.integerField <= 2147483647;
const isValidBoolean = typeof data.booleanField === 'boolean';
const isValidDate = data.dateField instanceof Date && !isNaN(data.dateField.getTime());
const isValidOptional = data.optionalField === null || typeof data.optionalField === 'string';
return isValidString && isValidInteger && isValidBoolean && isValidDate && isValidOptional;
}
),
{
numRuns: 100,
timeout: 5000
}
);
});
test('Property 32: Database transaction properties should be maintained', async () => {
await fc.assert(
fc.asyncProperty(
fc.array(
fc.record({
operation: fc.constantFrom('INSERT', 'UPDATE', 'DELETE', 'SELECT'),
table: fc.constantFrom('users', 'projects', 'pages', 'assets'),
data: fc.record({
id: fc.string({ minLength: 1, maxLength: 50 }),
timestamp: fc.date()
})
}),
{ minLength: 1, maxLength: 10 }
),
async (operations) => {
// Test transaction properties: atomicity, consistency, isolation, durability (ACID)
// Atomicity: All operations in a transaction should succeed or fail together
const hasConsistentOperations = operations.every(op =>
['INSERT', 'UPDATE', 'DELETE', 'SELECT'].includes(op.operation)
);
// Consistency: Data should maintain referential integrity
const hasValidTables = operations.every(op =>
['users', 'projects', 'pages', 'assets'].includes(op.table)
);
// Isolation: Operations should not interfere with each other
const hasUniqueIds = new Set(operations.map(op => op.data.id)).size === operations.length;
// Durability: Committed transactions should persist
const hasValidTimestamps = operations.every(op =>
op.data.timestamp instanceof Date && !isNaN(op.data.timestamp.getTime())
);
return hasConsistentOperations && hasValidTables && hasUniqueIds && hasValidTimestamps;
}
),
{
numRuns: 100,
timeout: 5000
}
);
});
});
+53
View File
@@ -0,0 +1,53 @@
import { PrismaClient } from '@prisma/client';
// Simple logger for database operations
const logger = {
info: (message: string, ...args: any[]) => console.log(`[INFO] ${message}`, ...args),
error: (message: string, ...args: any[]) => console.error(`[ERROR] ${message}`, ...args),
warn: (message: string, ...args: any[]) => console.warn(`[WARN] ${message}`, ...args),
};
let prisma: PrismaClient;
declare global {
var __prisma: PrismaClient | undefined;
}
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient({
log: ['error', 'warn'],
});
} else {
if (!global.__prisma) {
global.__prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
});
}
prisma = global.__prisma;
}
// Test database connectivity
export async function testDatabaseConnection(): Promise<boolean> {
try {
await prisma.$connect();
await prisma.$queryRaw`SELECT 1`;
logger.info('Database connection successful');
return true;
} catch (error) {
logger.error('Database connection failed:', error);
return false;
}
}
// Graceful shutdown
export async function disconnectDatabase(): Promise<void> {
try {
await prisma.$disconnect();
logger.info('Database disconnected successfully');
} catch (error) {
logger.error('Error disconnecting from database:', error);
}
}
export { prisma };
export default prisma;
+2
View File
@@ -0,0 +1,2 @@
export * from './client';
export * from '@prisma/client';
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*", "prisma/**/*"],
"exclude": ["dist", "node_modules"]
}
File diff suppressed because one or more lines are too long