Initial commit: SDI SaaS Platform foundation

- Complete monorepo structure with pnpm workspaces
- Prisma database schema with 20+ entities
- NestJS API with 9 core modules
- BullMQ orchestration worker
- AWS and Azure provider adapters
- Docker Compose infrastructure
- Complete documentation
This commit is contained in:
austindebest
2026-04-20 00:00:59 +01:00
commit d62468adf9
69 changed files with 10136 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================================================
// TENANCY & IDENTITY
// ============================================================================
model Tenant {
id String @id @default(uuid())
name String
slug String @unique
status String @default("active") // active, suspended, terminated
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
orders ServiceOrder[]
services Service[]
apiKeys ApiKey[]
webhookEndpoints WebhookEndpoint[]
providerAccounts ProviderAccount[]
invoices Invoice[]
@@map("tenants")
}
model User {
id String @id @default(uuid())
tenantId String
email String @unique
name String
status String @default("active")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
roles Role[]
orders ServiceOrder[]
@@map("users")
}
model Role {
id String @id @default(uuid())
userId String
name String // admin, operator, viewer
permissions Json @default("{}")
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("roles")
}
// ============================================================================
// PROVIDERS & CONNECTIVITY
// ============================================================================
model Provider {
id String @id @default(uuid())
name String
type String // aws, azure, carrier, exchange
status String @default("active")
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
endpoints Endpoint[]
productOfferings ProductOffering[]
orders ServiceOrder[]
accounts ProviderAccount[]
@@map("providers")
}
model ProviderAccount {
id String @id @default(uuid())
tenantId String
providerId String
accountId String
credentials Json // encrypted
status String @default("active")
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
provider Provider @relation(fields: [providerId], references: [id])
@@unique([tenantId, providerId, accountId])
@@map("provider_accounts")
}
model Endpoint {
id String @id @default(uuid())
providerId String
kind String // cloud_region, datacenter, exchange, customer_site
region String?
metro String?
location String?
status String @default("available")
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
provider Provider @relation(fields: [providerId], references: [id])
sourceOrders ServiceOrder[] @relation("SourceEndpoint")
targetOrders ServiceOrder[] @relation("TargetEndpoint")
@@map("endpoints")
}
// ============================================================================
// CATALOG & PRODUCTS
// ============================================================================
model ProductOffering {
id String @id @default(uuid())
providerId String
name String
description String?
type String // cloud_interconnect, multi_cloud, partner_connect
status String @default("active")
pricing Json @default("{}")
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
provider Provider @relation(fields: [providerId], references: [id])
orders ServiceOrder[]
@@map("product_offerings")
}
// ============================================================================
// ORDERS & SERVICES
// ============================================================================
model Quote {
id String @id @default(uuid())
tenantId String
productOfferingId String
sourceEndpointId String
targetEndpointId String
bandwidthMbps Int
monthlyRecurring Decimal @db.Decimal(10, 2)
setupFee Decimal @db.Decimal(10, 2)
currency String @default("USD")
validUntil DateTime
status String @default("draft") // draft, valid, expired, accepted
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("quotes")
}
model ServiceOrder {
id String @id @default(uuid())
tenantId String
userId String
productOfferingId String
providerId String
sourceEndpointId String
targetEndpointId String
bandwidthMbps Int
status String @default("draft")
externalReference String?
quoteId String?
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
user User @relation(fields: [userId], references: [id])
productOffering ProductOffering @relation(fields: [productOfferingId], references: [id])
provider Provider @relation(fields: [providerId], references: [id])
sourceEndpoint Endpoint @relation("SourceEndpoint", fields: [sourceEndpointId], references: [id])
targetEndpoint Endpoint @relation("TargetEndpoint", fields: [targetEndpointId], references: [id])
service Service?
provisioningTasks ProvisioningTask[]
@@map("service_orders")
}
model Service {
id String @id @default(uuid())
orderId String @unique
tenantId String
status String @default("provisioning")
activatedAt DateTime?
suspendedAt DateTime?
terminatedAt DateTime?
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order ServiceOrder @relation(fields: [orderId], references: [id])
tenant Tenant @relation(fields: [tenantId], references: [id])
usageRecords UsageRecord[]
inventoryRecords InventoryRecord[]
@@map("services")
}
// ============================================================================
// PROVISIONING & ORCHESTRATION
// ============================================================================
model ProvisioningTask {
id String @id @default(uuid())
orderId String
taskType String // validate, provision, configure, activate
status String @default("pending")
adapterName String?
externalTaskId String?
retryCount Int @default(0)
maxRetries Int @default(3)
error String?
metadata Json @default("{}")
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order ServiceOrder @relation(fields: [orderId], references: [id])
@@map("provisioning_tasks")
}
// ============================================================================
// INVENTORY & MONITORING
// ============================================================================
model InventoryRecord {
id String @id @default(uuid())
serviceId String
recordType String // connection, circuit, port
externalId String
providerData Json @default("{}")
status String
lastSyncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@map("inventory_records")
}
// ============================================================================
// BILLING
// ============================================================================
model UsageRecord {
id String @id @default(uuid())
serviceId String
recordType String // bandwidth, port_hour, data_transfer
quantity Decimal @db.Decimal(15, 4)
unit String
startTime DateTime
endTime DateTime
metadata Json @default("{}")
createdAt DateTime @default(now())
service Service @relation(fields: [serviceId], references: [id])
@@map("usage_records")
}
model Invoice {
id String @id @default(uuid())
tenantId String
invoiceNumber String @unique
status String @default("draft")
subtotal Decimal @db.Decimal(10, 2)
tax Decimal @db.Decimal(10, 2)
total Decimal @db.Decimal(10, 2)
currency String @default("USD")
periodStart DateTime
periodEnd DateTime
dueDate DateTime
paidAt DateTime?
metadata Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
@@map("invoices")
}
// ============================================================================
// API & WEBHOOKS
// ============================================================================
model ApiKey {
id String @id @default(uuid())
tenantId String
name String
keyHash String @unique
scopes Json @default("[]")
status String @default("active")
lastUsedAt DateTime?
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
@@map("api_keys")
}
model WebhookEndpoint {
id String @id @default(uuid())
tenantId String
url String
events Json @default("[]")
secret String
status String @default("active")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
deliveries WebhookDelivery[]
@@map("webhook_endpoints")
}
model WebhookDelivery {
id String @id @default(uuid())
endpointId String
eventType String
payload Json
status String @default("pending")
attempts Int @default(0)
lastAttempt DateTime?
response String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id])
@@map("webhook_deliveries")
}
// ============================================================================
// AUDIT & INCIDENTS
// ============================================================================
model AuditEvent {
id String @id @default(uuid())
aggregateType String
aggregateId String
eventType String
actorId String?
actorType String?
payload Json @default("{}")
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([aggregateType, aggregateId])
@@index([eventType])
@@index([createdAt])
@@map("audit_events")
}
model Incident {
id String @id @default(uuid())
tenantId String?
serviceId String?
severity String // critical, high, medium, low
status String @default("open")
title String
description String
metadata Json @default("{}")
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("incidents")
}

View File

@@ -0,0 +1,160 @@
import { PrismaClient } from '@sdi/database';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Create providers
const awsProvider = await prisma.provider.create({
data: {
name: 'Amazon Web Services',
type: 'aws',
status: 'active',
metadata: {
description: 'AWS Direct Connect',
},
},
});
const azureProvider = await prisma.provider.create({
data: {
name: 'Microsoft Azure',
type: 'azure',
status: 'active',
metadata: {
description: 'Azure ExpressRoute',
},
},
});
console.log('✓ Created providers');
// Create AWS endpoints
const awsEndpoints = [
{ region: 'us-east-1', metro: 'Washington DC', location: 'Equinix DC2' },
{ region: 'us-west-2', metro: 'Seattle', location: 'Equinix SE2' },
{ region: 'eu-west-1', metro: 'Dublin', location: 'Equinix DB3' },
];
for (const ep of awsEndpoints) {
await prisma.endpoint.create({
data: {
providerId: awsProvider.id,
kind: 'cloud_region',
region: ep.region,
metro: ep.metro,
location: ep.location,
status: 'available',
metadata: { provider: 'aws' },
},
});
}
console.log('✓ Created AWS endpoints');
// Create Azure endpoints
const azureEndpoints = [
{ region: 'eastus', metro: 'Washington DC', location: 'Equinix DC2' },
{ region: 'westus2', metro: 'Seattle', location: 'Equinix SE2' },
{ region: 'westeurope', metro: 'Amsterdam', location: 'Equinix AM3' },
];
for (const ep of azureEndpoints) {
await prisma.endpoint.create({
data: {
providerId: azureProvider.id,
kind: 'cloud_region',
region: ep.region,
metro: ep.metro,
location: ep.location,
status: 'available',
metadata: { provider: 'azure' },
},
});
}
console.log('✓ Created Azure endpoints');
// Create product offerings
await prisma.productOffering.create({
data: {
providerId: awsProvider.id,
name: 'AWS Direct Connect - 1Gbps',
description: 'Dedicated 1Gbps connection to AWS',
type: 'cloud_interconnect',
status: 'active',
pricing: {
setupFee: 500,
monthlyRecurring: 50,
currency: 'USD',
},
},
});
await prisma.productOffering.create({
data: {
providerId: azureProvider.id,
name: 'Azure ExpressRoute - 1Gbps',
description: 'Dedicated 1Gbps connection to Azure',
type: 'cloud_interconnect',
status: 'active',
pricing: {
setupFee: 600,
monthlyRecurring: 60,
currency: 'USD',
},
},
});
console.log('✓ Created product offerings');
// Create demo tenant
const tenant = await prisma.tenant.create({
data: {
name: 'Demo Corporation',
slug: 'demo-corp',
status: 'active',
},
});
console.log('✓ Created demo tenant');
// Create demo user
const user = await prisma.user.create({
data: {
tenantId: tenant.id,
email: 'admin@demo-corp.com',
name: 'Demo Admin',
status: 'active',
},
});
await prisma.role.create({
data: {
userId: user.id,
name: 'admin',
permissions: {
orders: ['create', 'read', 'update', 'delete'],
services: ['read', 'modify', 'suspend', 'terminate'],
},
},
});
console.log('✓ Created demo user with admin role');
console.log('\n✅ Database seeded successfully!');
console.log('\nDemo credentials:');
console.log(` Tenant: ${tenant.slug}`);
console.log(` Email: ${user.email}`);
console.log(` Tenant ID: ${tenant.id}`);
}
main()
.catch((e) => {
console.error('Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});