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 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/sdi_saas?schema=public"

View File

@@ -0,0 +1,20 @@
{
"name": "@sdi/database",
"version": "0.1.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio",
"db:push": "prisma db push",
"db:seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^5.9.0"
},
"devDependencies": {
"prisma": "^5.9.0",
"tsx": "^4.21.0"
}
}

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();
});

View File

@@ -0,0 +1,2 @@
export { PrismaClient } from '@prisma/client';
export * from '@prisma/client';

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"extends": "../../tsconfig.json",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,13 @@
{
"name": "@sdi/shared-types",
"version": "0.1.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,208 @@
// ============================================================================
// SERVICE ORDER TYPES
// ============================================================================
export type ServiceOrderStatus =
| 'draft'
| 'submitted'
| 'validating'
| 'quoted'
| 'approved'
| 'queued'
| 'provisioning'
| 'active'
| 'failed'
| 'suspended'
| 'terminated';
export type ServiceStatus =
| 'pending'
| 'provisioning'
| 'active'
| 'suspended'
| 'failed'
| 'terminated';
export type ProvisioningTaskStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'retrying';
// ============================================================================
// PROVIDER ADAPTER TYPES
// ============================================================================
export interface ServiceIntent {
sourceEndpointId: string;
targetEndpointId: string;
bandwidthMbps: number;
metadata?: Record<string, any>;
}
export interface ValidationResult {
ok: boolean;
errors?: string[];
warnings?: string[];
}
export interface QuoteResult {
monthlyRecurring: number;
setupFee: number;
currency: string;
validUntil: Date;
metadata?: Record<string, any>;
}
export interface ProvisionRequest {
sourceEndpointId: string;
targetEndpointId: string;
bandwidthMbps: number;
metadata?: Record<string, any>;
}
export interface ProvisionResponse {
success: boolean;
externalServiceId?: string;
error?: string;
metadata?: Record<string, any>;
}
export interface ModifyRequest {
externalServiceId: string;
bandwidthMbps?: number;
metadata?: Record<string, any>;
}
export interface ModifyResponse {
success: boolean;
error?: string;
}
export interface ActionResult {
success: boolean;
error?: string;
}
export interface ProviderAdapter {
validate(payload: ServiceIntent): Promise<ValidationResult>;
quote(payload: ServiceIntent): Promise<QuoteResult>;
provision(payload: ProvisionRequest): Promise<ProvisionResponse>;
getStatus(externalId: string): Promise<ServiceStatus>;
modify(payload: ModifyRequest): Promise<ModifyResponse>;
suspend(externalId: string): Promise<ActionResult>;
terminate(externalId: string): Promise<ActionResult>;
syncInventory?(): Promise<void>;
}
// ============================================================================
// WEBHOOK EVENT TYPES
// ============================================================================
export type WebhookEventType =
| 'quote.ready'
| 'order.accepted'
| 'order.rejected'
| 'service.provisioning.started'
| 'service.provider.pending'
| 'service.active'
| 'service.failed'
| 'service.modified'
| 'service.suspended'
| 'service.terminated'
| 'invoice.generated'
| 'incident.created';
export interface WebhookEvent {
id: string;
type: WebhookEventType;
timestamp: Date;
data: Record<string, any>;
}
// ============================================================================
// API REQUEST/RESPONSE TYPES
// ============================================================================
export interface CreateQuoteRequest {
productOfferingId: string;
sourceEndpointId: string;
targetEndpointId: string;
bandwidthMbps: number;
}
export interface CreateOrderRequest {
quoteId?: string;
productOfferingId: string;
sourceEndpointId: string;
targetEndpointId: string;
bandwidthMbps: number;
}
export interface ModifyServiceRequest {
bandwidthMbps?: number;
}
// ============================================================================
// ORCHESTRATION EVENT TYPES
// ============================================================================
export type OrchestrationEventType =
| 'order.created'
| 'order.validated'
| 'quote.generated'
| 'order.approved'
| 'provisioning.started'
| 'provider.request.sent'
| 'provider.pending'
| 'provider.completed'
| 'service.active'
| 'service.failed'
| 'billing.metering.started'
| 'inventory.sync.completed'
| 'incident.opened';
export interface OrchestrationEvent {
type: OrchestrationEventType;
orderId: string;
tenantId: string;
payload: Record<string, any>;
timestamp: Date;
}
// ============================================================================
// PROVIDER TYPES
// ============================================================================
export type ProviderType = 'aws' | 'azure' | 'carrier' | 'exchange';
export type EndpointKind =
| 'cloud_region'
| 'datacenter'
| 'exchange'
| 'customer_site';
// ============================================================================
// BILLING TYPES
// ============================================================================
export interface PricingRule {
setupFee: number;
monthlyRecurring: number;
bandwidthTiers?: Array<{
minMbps: number;
maxMbps: number;
pricePerMbps: number;
}>;
currency: string;
}
export interface UsageMetrics {
serviceId: string;
bandwidthUsageMbps: number;
dataTransferGB: number;
uptimePercentage: number;
periodStart: Date;
periodEnd: Date;
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"extends": "../../tsconfig.json",
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}