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

3
apps/worker/.env.example Normal file
View File

@@ -0,0 +1,3 @@
NODE_ENV=development
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/sdi_saas?schema=public"
REDIS_URL="redis://localhost:6379"

25
apps/worker/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM node:20-alpine AS base
RUN npm install -g pnpm@8.15.0
FROM base AS dependencies
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY apps/worker/package.json ./apps/worker/
COPY packages/database/package.json ./packages/database/
COPY packages/shared-types/package.json ./packages/shared-types/
RUN pnpm install --frozen-lockfile
FROM base AS build
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN pnpm --filter @sdi/database db:generate
RUN pnpm --filter @sdi/worker build
FROM base AS runtime
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/apps/worker/dist ./apps/worker/dist
COPY --from=build /app/packages ./packages
COPY apps/worker/package.json ./apps/worker/
CMD ["node", "apps/worker/dist/main.js"]

20
apps/worker/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "@sdi/worker",
"version": "0.1.0",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/main.ts",
"start": "node dist/main.js"
},
"dependencies": {
"@sdi/database": "workspace:*",
"@sdi/shared-types": "workspace:*",
"bullmq": "^5.1.0",
"ioredis": "^5.3.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,24 @@
import { ProviderAdapter } from '@sdi/shared-types';
import { AwsAdapter } from './aws-adapter';
import { AzureAdapter } from './azure-adapter';
export class AdapterRegistry {
private adapters: Map<string, ProviderAdapter> = new Map();
constructor() {
this.adapters.set('aws', new AwsAdapter());
this.adapters.set('azure', new AzureAdapter());
}
get(providerType: string): ProviderAdapter {
const adapter = this.adapters.get(providerType);
if (!adapter) {
throw new Error(`No adapter found for provider type: ${providerType}`);
}
return adapter;
}
register(providerType: string, adapter: ProviderAdapter) {
this.adapters.set(providerType, adapter);
}
}

View File

@@ -0,0 +1,96 @@
import {
ProviderAdapter,
ServiceIntent,
ValidationResult,
QuoteResult,
ProvisionRequest,
ProvisionResponse,
ModifyRequest,
ModifyResponse,
ActionResult,
ServiceStatus,
} from '@sdi/shared-types';
export class AwsAdapter implements ProviderAdapter {
async validate(payload: ServiceIntent): Promise<ValidationResult> {
// TODO: Implement AWS Direct Connect validation
// - Check if source/target endpoints are valid
// - Verify bandwidth is supported
// - Check location availability
console.log('AWS: Validating service intent', payload);
// Mock validation
return {
ok: true,
warnings: ['AWS adapter is in mock mode'],
};
}
async quote(payload: ServiceIntent): Promise<QuoteResult> {
// TODO: Implement AWS pricing calculation
// - Get Direct Connect pricing for bandwidth
// - Calculate port hours
// - Add data transfer costs
console.log('AWS: Generating quote', payload);
return {
monthlyRecurring: payload.bandwidthMbps * 0.05,
setupFee: 500,
currency: 'USD',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
};
}
async provision(payload: ProvisionRequest): Promise<ProvisionResponse> {
// TODO: Implement AWS Direct Connect provisioning
// - Create connection via AWS SDK
// - Configure virtual interface
// - Set up BGP peering
console.log('AWS: Provisioning service', payload);
// Mock provisioning
const externalServiceId = `dx-${Date.now()}`;
return {
success: true,
externalServiceId,
metadata: {
connectionId: externalServiceId,
vlan: 100,
bgpAsn: 65000,
},
};
}
async getStatus(externalId: string): Promise<ServiceStatus> {
// TODO: Implement status check via AWS SDK
console.log('AWS: Getting status for', externalId);
return 'active';
}
async modify(payload: ModifyRequest): Promise<ModifyResponse> {
// TODO: Implement bandwidth modification
console.log('AWS: Modifying service', payload);
return { success: true };
}
async suspend(externalId: string): Promise<ActionResult> {
// TODO: Implement service suspension
console.log('AWS: Suspending service', externalId);
return { success: true };
}
async terminate(externalId: string): Promise<ActionResult> {
// TODO: Implement connection deletion via AWS SDK
console.log('AWS: Terminating service', externalId);
return { success: true };
}
async syncInventory(): Promise<void> {
// TODO: Sync AWS Direct Connect inventory
console.log('AWS: Syncing inventory');
}
}

View File

@@ -0,0 +1,94 @@
import {
ProviderAdapter,
ServiceIntent,
ValidationResult,
QuoteResult,
ProvisionRequest,
ProvisionResponse,
ModifyRequest,
ModifyResponse,
ActionResult,
ServiceStatus,
} from '@sdi/shared-types';
export class AzureAdapter implements ProviderAdapter {
async validate(payload: ServiceIntent): Promise<ValidationResult> {
// TODO: Implement Azure ExpressRoute validation
// - Check peering location availability
// - Verify SKU and bandwidth tier
// - Validate circuit configuration
console.log('Azure: Validating service intent', payload);
return {
ok: true,
warnings: ['Azure adapter is in mock mode'],
};
}
async quote(payload: ServiceIntent): Promise<QuoteResult> {
// TODO: Implement Azure ExpressRoute pricing
// - Get circuit pricing by SKU
// - Calculate metered vs unlimited data
// - Add premium add-on costs if needed
console.log('Azure: Generating quote', payload);
return {
monthlyRecurring: payload.bandwidthMbps * 0.06,
setupFee: 600,
currency: 'USD',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
};
}
async provision(payload: ProvisionRequest): Promise<ProvisionResponse> {
// TODO: Implement Azure ExpressRoute provisioning
// - Create ExpressRoute circuit
// - Configure peering (private/Microsoft)
// - Set up route filters
console.log('Azure: Provisioning service', payload);
const externalServiceId = `er-${Date.now()}`;
return {
success: true,
externalServiceId,
metadata: {
circuitId: externalServiceId,
serviceKey: `sk-${Date.now()}`,
peeringLocation: 'mock-location',
},
};
}
async getStatus(externalId: string): Promise<ServiceStatus> {
// TODO: Check circuit provisioning state via Azure SDK
console.log('Azure: Getting status for', externalId);
return 'active';
}
async modify(payload: ModifyRequest): Promise<ModifyResponse> {
// TODO: Implement circuit modification
console.log('Azure: Modifying service', payload);
return { success: true };
}
async suspend(externalId: string): Promise<ActionResult> {
// TODO: Implement circuit suspension
console.log('Azure: Suspending service', externalId);
return { success: true };
}
async terminate(externalId: string): Promise<ActionResult> {
// TODO: Delete ExpressRoute circuit via Azure SDK
console.log('Azure: Terminating service', externalId);
return { success: true };
}
async syncInventory(): Promise<void> {
// TODO: Sync Azure ExpressRoute inventory
console.log('Azure: Syncing inventory');
}
}

62
apps/worker/src/main.ts Normal file
View File

@@ -0,0 +1,62 @@
import { Worker, Queue } from 'bullmq';
import { PrismaClient } from '@sdi/database';
import { ProvisioningOrchestrator } from './orchestration/provisioning-orchestrator';
import Redis from 'ioredis';
const connection = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
maxRetriesPerRequest: null,
});
const prisma = new PrismaClient();
const orchestrator = new ProvisioningOrchestrator(prisma);
// Provisioning queue worker
const provisioningWorker = new Worker(
'provisioning',
async (job) => {
console.log(`Processing job ${job.id}: ${job.name}`);
switch (job.name) {
case 'provision-order':
await orchestrator.provisionOrder(job.data.orderId);
break;
case 'modify-service':
await orchestrator.modifyService(job.data.serviceId, job.data.changes);
break;
case 'suspend-service':
await orchestrator.suspendService(job.data.serviceId);
break;
case 'terminate-service':
await orchestrator.terminateService(job.data.serviceId);
break;
default:
throw new Error(`Unknown job type: ${job.name}`);
}
},
{
connection,
concurrency: 5,
limiter: {
max: 10,
duration: 1000,
},
},
);
provisioningWorker.on('completed', (job) => {
console.log(`✓ Job ${job.id} completed successfully`);
});
provisioningWorker.on('failed', (job, err) => {
console.error(`✗ Job ${job?.id} failed:`, err.message);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing worker...');
await provisioningWorker.close();
await prisma.$disconnect();
process.exit(0);
});
console.log('🔧 Worker started and listening for jobs...');

View File

@@ -0,0 +1,168 @@
import { PrismaClient } from '@sdi/database';
import { ServiceOrderStatus } from '@sdi/shared-types';
import { AdapterRegistry } from '../adapters/adapter-registry';
export class ProvisioningOrchestrator {
private adapterRegistry: AdapterRegistry;
constructor(private prisma: PrismaClient) {
this.adapterRegistry = new AdapterRegistry();
}
async provisionOrder(orderId: string) {
const order = await this.prisma.serviceOrder.findUnique({
where: { id: orderId },
include: {
sourceEndpoint: true,
targetEndpoint: true,
provider: true,
},
});
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
try {
// Update status to validating
await this.updateOrderStatus(orderId, 'validating');
// Get provider adapter
const adapter = this.adapterRegistry.get(order.provider.type);
// Validate service intent
const validation = await adapter.validate({
sourceEndpointId: order.sourceEndpointId,
targetEndpointId: order.targetEndpointId,
bandwidthMbps: order.bandwidthMbps,
});
if (!validation.ok) {
await this.updateOrderStatus(orderId, 'failed');
await this.logAudit(orderId, 'validation_failed', { errors: validation.errors });
return;
}
// Update status to provisioning
await this.updateOrderStatus(orderId, 'provisioning');
// Provision service
const result = await adapter.provision({
sourceEndpointId: order.sourceEndpointId,
targetEndpointId: order.targetEndpointId,
bandwidthMbps: order.bandwidthMbps,
});
if (!result.success) {
await this.updateOrderStatus(orderId, 'failed');
await this.logAudit(orderId, 'provision_failed', { error: result.error });
return;
}
// Update order with external reference
await this.prisma.serviceOrder.update({
where: { id: orderId },
data: { externalReference: result.externalServiceId },
});
// Create service record
const service = await this.prisma.service.create({
data: {
orderId,
tenantId: order.tenantId,
status: 'active',
activatedAt: new Date(),
},
});
// Update order status to active
await this.updateOrderStatus(orderId, 'active');
await this.logAudit(orderId, 'service_active', { serviceId: service.id });
console.log(`✓ Order ${orderId} provisioned successfully`);
} catch (error) {
console.error(`✗ Failed to provision order ${orderId}:`, error);
await this.updateOrderStatus(orderId, 'failed');
await this.logAudit(orderId, 'provision_error', { error: error.message });
throw error;
}
}
async modifyService(serviceId: string, changes: { bandwidthMbps?: number }) {
const service = await this.prisma.service.findUnique({
where: { id: serviceId },
include: { order: { include: { provider: true } } },
});
if (!service) {
throw new Error(`Service ${serviceId} not found`);
}
const adapter = this.adapterRegistry.get(service.order.provider.type);
const result = await adapter.modify({
externalServiceId: service.order.externalReference!,
bandwidthMbps: changes.bandwidthMbps,
});
if (result.success) {
await this.logAudit(serviceId, 'service_modified', changes);
}
}
async suspendService(serviceId: string) {
const service = await this.prisma.service.findUnique({
where: { id: serviceId },
include: { order: { include: { provider: true } } },
});
if (!service) {
throw new Error(`Service ${serviceId} not found`);
}
const adapter = this.adapterRegistry.get(service.order.provider.type);
await adapter.suspend(service.order.externalReference!);
await this.prisma.service.update({
where: { id: serviceId },
data: { status: 'suspended', suspendedAt: new Date() },
});
}
async terminateService(serviceId: string) {
const service = await this.prisma.service.findUnique({
where: { id: serviceId },
include: { order: { include: { provider: true } } },
});
if (!service) {
throw new Error(`Service ${serviceId} not found`);
}
const adapter = this.adapterRegistry.get(service.order.provider.type);
await adapter.terminate(service.order.externalReference!);
await this.prisma.service.update({
where: { id: serviceId },
data: { status: 'terminated', terminatedAt: new Date() },
});
}
private async updateOrderStatus(orderId: string, status: ServiceOrderStatus) {
await this.prisma.serviceOrder.update({
where: { id: orderId },
data: { status, updatedAt: new Date() },
});
}
private async logAudit(aggregateId: string, eventType: string, payload: any) {
await this.prisma.auditEvent.create({
data: {
aggregateType: 'order',
aggregateId,
eventType,
actorType: 'system',
payload,
},
});
}
}

13
apps/worker/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"paths": {
"@sdi/database": ["../../packages/database/src"],
"@sdi/shared-types": ["../../packages/shared-types/src"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}