Initial commit

This commit is contained in:
2026-03-10 21:30:52 -03:00
commit 72a4f0be26
145 changed files with 14881 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'prisma/config';
/**
* Prisma 7 CLI configuration.
*
* datasource.url is read from DATABASE_URL at CLI runtime.
* Falls back to a placeholder so that `prisma generate` works without a live
* database (no connection is made during code generation).
*/
export default defineConfig({
schema: '../prisma/schema.prisma',
migrations: {
path: '../prisma/migrations',
seed: 'tsx prisma/seed.ts',
},
datasource: {
url: process.env['DATABASE_URL'] ?? 'postgresql://placeholder@localhost:5432/placeholder',
},
});

1
packages/db/.env.example Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="postgresql://dwellops:dwellops@localhost:5432/dwellops_dev?schema=public"

View File

@@ -0,0 +1,3 @@
import { base } from '@dwellops/config/eslint';
export default base;

41
packages/db/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "@dwellops/db",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./client": {
"types": "./src/client.ts",
"default": "./src/client.ts"
}
},
"scripts": {
"db:generate": "prisma generate",
"db:migrate": "prisma migrate deploy",
"db:migrate:dev": "prisma migrate dev",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts",
"lint": "eslint src",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@prisma/client": "catalog:",
"@prisma/adapter-pg": "catalog:",
"pg": "catalog:",
"@dwellops/types": "workspace:*"
},
"devDependencies": {
"prisma": "catalog:",
"eslint": "catalog:",
"@types/pg": "catalog:",
"@dwellops/config": "workspace:*",
"typescript": "catalog:",
"tsx": "catalog:",
"@types/node": "catalog:"
}
}

View File

@@ -0,0 +1,154 @@
// Prisma schema for dwellops-platform
// Better Auth requires User, Session, Account, and Verification models
// with specific field names — do not rename them.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
// ─── Better Auth required models ───────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
name String?
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
memberships Membership[]
@@map("users")
}
model Session {
id String @id
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model Account {
id String @id
userId String
accountId String
providerId String
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("accounts")
}
model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("verifications")
}
// ─── HOA domain models ─────────────────────────────────────────────────────
/// A homeowners association managed by the platform.
/// In self-hosted mode there is typically one HOA per deployment,
/// but the schema supports multiple to enable future SaaS evolution.
model Hoa {
id String @id @default(cuid())
name String
slug String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
units Unit[]
memberships Membership[]
@@map("hoas")
}
/// A dwelling unit within an HOA (apartment, townhouse, lot, etc.).
model Unit {
id String @id @default(cuid())
hoaId String
identifier String
address String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hoa Hoa @relation(fields: [hoaId], references: [id], onDelete: Cascade)
memberships Membership[]
@@unique([hoaId, identifier])
@@map("units")
}
/// Supported roles within an HOA.
enum Role {
ADMIN
BOARD_MEMBER
TREASURER
OWNER
TENANT
VIEWER
}
/// Connects a User to an HOA with a role, optionally scoped to a Unit.
model Membership {
id String @id @default(cuid())
userId String
hoaId String
unitId String?
role Role @default(OWNER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
hoa Hoa @relation(fields: [hoaId], references: [id], onDelete: Cascade)
unit Unit? @relation(fields: [unitId], references: [id], onDelete: SetNull)
@@unique([userId, hoaId, unitId])
@@map("memberships")
}
/// Immutable audit log for important platform actions.
model AuditLog {
id String @id @default(cuid())
/// Nullable to support system-initiated actions.
userId String?
action String
entityType String
entityId String?
payload Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@map("audit_logs")
}

View File

@@ -0,0 +1,52 @@
import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
const connectionString = process.env['DATABASE_URL'];
if (!connectionString) {
throw new Error('DATABASE_URL is required for seeding');
}
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
/**
* Seeds the local development database with minimal starter data.
* Not intended for production use.
*/
async function main(): Promise<void> {
console.log('Seeding database...');
const hoa = await prisma.hoa.upsert({
where: { slug: 'sunrise-ridge' },
create: {
name: 'Sunrise Ridge HOA',
slug: 'sunrise-ridge',
description: 'Development seed HOA',
},
update: {},
});
console.log(`HOA: ${hoa.name} (${hoa.id})`);
const unit = await prisma.unit.upsert({
where: { hoaId_identifier: { hoaId: hoa.id, identifier: '101' } },
create: {
hoaId: hoa.id,
identifier: '101',
address: '1 Sunrise Ridge Dr, Unit 101',
},
update: {},
});
console.log(`Unit: ${unit.identifier} (${unit.id})`);
console.log('Seeding complete.');
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());

44
packages/db/src/client.ts Normal file
View File

@@ -0,0 +1,44 @@
import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
declare global {
var __dwellops_prisma: PrismaClient | undefined; // required for global type augmentation
}
/**
* Creates a new PrismaClient instance using the PostgreSQL adapter.
* DATABASE_URL must be set before this module is imported.
*/
function createPrismaClient(): PrismaClient {
const connectionString = process.env['DATABASE_URL'];
if (!connectionString) {
throw new Error('DATABASE_URL environment variable is required for @dwellops/db');
}
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
return new PrismaClient({
adapter,
log:
process.env['NODE_ENV'] === 'development'
? ['query', 'warn', 'error']
: ['warn', 'error'],
});
}
/**
* Singleton PrismaClient instance.
*
* In development the client is stored on the global object to survive
* hot-module reloads. In production a fresh instance is created once.
*/
const prisma: PrismaClient = global.__dwellops_prisma ?? createPrismaClient();
if (process.env['NODE_ENV'] !== 'production') {
global.__dwellops_prisma = prisma;
}
export { prisma };
export default prisma;

22
packages/db/src/index.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* @dwellops/db — data access boundary.
*
* Only import from this package for database access. Never import from
* @prisma/client directly in apps/api or apps/web.
*/
export { prisma, default as db } from './client';
// Re-export Prisma types so consumers don't need to depend on @prisma/client.
export type {
User,
Session,
Account,
Verification,
Hoa,
Unit,
Membership,
AuditLog,
Role,
Prisma,
} from '@prisma/client';

View File

@@ -0,0 +1,9 @@
{
"extends": "@dwellops/config/tsconfig/node.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true
},
"include": ["src"]
}

File diff suppressed because one or more lines are too long