Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
744 changes: 744 additions & 0 deletions .azure/plan.md

Large diffs are not rendered by default.

330 changes: 122 additions & 208 deletions .github/workflows/infrastructure.yml

Large diffs are not rendered by default.

31 changes: 16 additions & 15 deletions .github/workflows/release-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ jobs:
id: validate
run: |
ENV="${{ matrix.environment }}"
PROJECT="whatssummarize"
RG="rg-${PROJECT}-${ENV}"
ORG="nl"
PROJECT="convolens"
# Naming pattern per ADR-0027 (no region suffix): {org}-{env}-{project}-{type}
BASE="${ORG}-${ENV}-${PROJECT}"
RG="${BASE}-rg"

echo "=============================================="
echo "Validating Azure Resources for Release"
Expand All @@ -112,8 +115,7 @@ jobs:
fi

# Check Key Vault
KV_NAME="kv${PROJECT}${ENV}"
KV_NAME="${KV_NAME//-/}"
KV_NAME="${BASE}-kv"
echo "Checking Key Vault: $KV_NAME..."
if ! az keyvault show --name "$KV_NAME" &>/dev/null; then
echo "❌ Key Vault not found"
Expand All @@ -133,9 +135,8 @@ jobs:
done
fi

# Check Storage Account
STORAGE_NAME="st${PROJECT}${ENV}"
STORAGE_NAME="${STORAGE_NAME//-/}"
# Check Storage Account (alphanumeric only)
STORAGE_NAME="${ORG}${ENV}${PROJECT}st"
echo "Checking Storage Account: $STORAGE_NAME..."
if ! az storage account show --name "$STORAGE_NAME" --resource-group "$RG" &>/dev/null; then
echo "❌ Storage account not found"
Expand All @@ -146,7 +147,7 @@ jobs:
fi

# Check Azure OpenAI
OPENAI_NAME="oai-${PROJECT}-${ENV}"
OPENAI_NAME="${BASE}-oai"
echo "Checking Azure OpenAI: $OPENAI_NAME..."
if ! az cognitiveservices account show --name "$OPENAI_NAME" --resource-group "$RG" &>/dev/null; then
echo "⚠️ Azure OpenAI not found (optional)"
Expand All @@ -164,7 +165,7 @@ jobs:
fi

# Check Cosmos DB
COSMOS_NAME="cosmos-${PROJECT}-${ENV}"
COSMOS_NAME="${BASE}-cosmos"
echo "Checking Cosmos DB: $COSMOS_NAME..."
if ! az cosmosdb show --name "$COSMOS_NAME" --resource-group "$RG" &>/dev/null; then
echo "⚠️ Cosmos DB not found (optional)"
Expand All @@ -174,7 +175,7 @@ jobs:
fi

# Check Redis
REDIS_NAME="redis-${PROJECT}-${ENV}"
REDIS_NAME="${BASE}-redis"
echo "Checking Redis Cache: $REDIS_NAME..."
if ! az redis show --name "$REDIS_NAME" --resource-group "$RG" &>/dev/null; then
echo "⚠️ Redis Cache not found (optional)"
Expand All @@ -184,7 +185,7 @@ jobs:
fi

# Check App Insights
APPI_NAME="appi-${PROJECT}-${ENV}"
APPI_NAME="${BASE}-appi"
echo "Checking Application Insights: $APPI_NAME..."
if ! az monitor app-insights component show --app "$APPI_NAME" --resource-group "$RG" &>/dev/null; then
echo "❌ Application Insights not found"
Expand All @@ -194,8 +195,8 @@ jobs:
echo "✅ Application Insights exists"
fi

# Check Container Apps
CAE_NAME="cae-${PROJECT}-${ENV}"
# Check Container Apps (names follow ${BASE}-{cae,api} per infra/terraform/env/{env}/main.tf)
CAE_NAME="${BASE}-cae"
echo "Checking Container Apps Environment: $CAE_NAME..."
if ! az containerapp env show --name "$CAE_NAME" --resource-group "$RG" &>/dev/null; then
echo "❌ Container Apps Environment not found"
Expand All @@ -205,10 +206,10 @@ jobs:
echo "✅ Container Apps Environment exists"

# Check API app
CA_NAME="ca-${PROJECT}-${ENV}-api"
CA_NAME="${BASE}-api"
if ! az containerapp show --name "$CA_NAME" --resource-group "$RG" &>/dev/null; then
echo "❌ API Container App not found"
MISSING="${MISSING}container-app-api,"
MISSING="${MISSING}container-apps-api,"
PASSED="false"
else
echo "✅ API Container App exists"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ logs
.tmp
actionlint
infra/bicep/*.json
**/.terraform/
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"pg": "^8.16.3",
"puppeteer": "^21.0.0",
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.2.2",
Expand Down
49 changes: 40 additions & 9 deletions apps/api/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
import { DataSource } from 'typeorm';
import type { DataSourceOptions } from 'typeorm';
import { Message } from '../db/entities/Message';
import { Group } from '../db/entities/Group';
import { User } from '../db/entities/User';
import { logger } from '../utils/logger';

export const AppDataSource = new DataSource({
type: 'sqlite',
database: 'database.sqlite',
synchronize: process.env.NODE_ENV !== 'production', // Auto-create tables in dev/test
logging: process.env.NODE_ENV === 'development',
const isProduction = process.env.NODE_ENV === 'production';
const dbType = process.env.DB_TYPE || 'sqlite';
const migrationsRun = process.env.DB_MIGRATIONS_RUN === undefined
? isProduction
: process.env.DB_MIGRATIONS_RUN === 'true';

const commonOptions = {
synchronize: !isProduction && process.env.DB_SYNCHRONIZE !== 'false',
logging: process.env.DB_LOGGING === 'true' || process.env.NODE_ENV === 'development',
entities: [Message, Group, User],
migrations: ['dist/db/migrations/*.js'],
migrationsRun: process.env.NODE_ENV === 'production',
migrationsRun,
subscribers: [],
cache: {
duration: 1000 * 30, // 30 seconds
duration: 1000 * 30,
},
// Enable WAL mode for better concurrency
};

const sqliteOptions: DataSourceOptions = {
...commonOptions,
type: 'sqlite',
database: process.env.DATABASE_PATH || 'database.sqlite',
extra: {
connection: {
pragma: 'journal_mode = WAL',
},
},
});
};

const postgresOptions: DataSourceOptions = {
...commonOptions,
type: 'postgres',
host: process.env.DB_HOST,
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'convolens',
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
};

const getDataSourceOptions = (): DataSourceOptions => {
if (dbType === 'postgres') {
return postgresOptions;
}

return sqliteOptions;
};

export const AppDataSource = new DataSource(getDataSourceOptions());

export const initializeDatabase = async (): Promise<void> => {
if (AppDataSource.isInitialized) {
Expand Down
51 changes: 41 additions & 10 deletions apps/api/src/config/datasource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataSource } from 'typeorm';
import type { DataSourceOptions } from 'typeorm';
import { User } from '../db/entities/User';
import { Group } from '../db/entities/Group';
import { Message } from '../db/entities/Message';
Expand All @@ -9,28 +10,58 @@ import dotenv from 'dotenv';
dotenv.config();

const isProduction = process.env.NODE_ENV === 'production';
const dbType = process.env.DB_TYPE || 'sqlite';
const migrationsRun = process.env.DB_MIGRATIONS_RUN === undefined
? isProduction
: process.env.DB_MIGRATIONS_RUN === 'true';

// Database configuration
export const AppDataSource = new DataSource({
type: 'sqlite',
database: process.env.DATABASE_PATH || 'database.sqlite',
synchronize: process.env.NODE_ENV !== 'production', // Auto-create tables in dev/test
logging: process.env.NODE_ENV === 'development' ? 'all' : ['error', 'warn'],
const commonOptions = {
synchronize: !isProduction && process.env.DB_SYNCHRONIZE !== 'false',
logging: process.env.DB_LOGGING === 'true' || process.env.NODE_ENV === 'development'
? 'all'
: ['error', 'warn'],
logger: isProduction ? 'file' : 'debug',
entities: [User, Group, Message],
migrations: ['dist/db/migrations/*.js'],
migrationsRun: process.env.NODE_ENV === 'production',
migrationsRun,
migrationsTableName: 'migrations',
cache: {
duration: 1000 * 30, // 30 seconds
duration: 1000 * 30,
},
// Enable WAL mode for better concurrency in SQLite
} satisfies Partial<DataSourceOptions>;

const sqliteOptions: DataSourceOptions = {
...commonOptions,
type: 'sqlite',
database: process.env.DATABASE_PATH || 'database.sqlite',
extra: {
connection: {
pragma: 'journal_mode = WAL',
},
},
});
};

const postgresOptions: DataSourceOptions = {
...commonOptions,
type: 'postgres',
host: process.env.DB_HOST,
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'convolens',
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
};

const getDataSourceOptions = (): DataSourceOptions => {
if (dbType === 'postgres') {
return postgresOptions;
}

return sqliteOptions;
};

// Database configuration
export const AppDataSource = new DataSource(getDataSourceOptions());

// Initialize the data source
export const initializeDataSource = async (): Promise<void> => {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/db/entities/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export class Group {
@Column({ type: 'simple-json', nullable: true })
metadata?: GroupMetadata;

@CreateDateColumn({ type: 'datetime' })
@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn({ type: 'datetime' })
@UpdateDateColumn()
updatedAt: Date;

@Column({ type: 'datetime', nullable: true })
@Column({ nullable: true })
archivedAt?: Date;
}
6 changes: 3 additions & 3 deletions apps/api/src/db/entities/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ export class Message {
@Column({ type: 'uuid' })
groupId: string;

@CreateDateColumn({ type: 'datetime' })
@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn({ type: 'datetime' })
@UpdateDateColumn()
updatedAt: Date;

@Column({ type: 'datetime', nullable: true })
@Column({ nullable: true })
deletedAt?: Date;
}
12 changes: 8 additions & 4 deletions apps/api/src/db/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BeforeUpdate,
OneToMany
} from 'typeorm';
import type { Relation } from 'typeorm';
import bcrypt from 'bcryptjs';
import { Group } from './Group';
import { Message } from './Message';
Expand Down Expand Up @@ -47,15 +48,18 @@ export class User {
lastLogin?: Date;

@OneToMany(() => Group, group => group.owner)
ownedGroups: Group[];
ownedGroups: Relation<Group[]>;

@OneToMany(() => Group, group => group.members)
groups: Relation<Group[]>;

@OneToMany(() => Message, message => message.sender)
messages: Message[];
messages: Relation<Message[]>;

@CreateDateColumn({ type: 'datetime' })
@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn({ type: 'datetime' })
@UpdateDateColumn()
updatedAt: Date;

@BeforeInsert()
Expand Down
8 changes: 3 additions & 5 deletions docs/AZURE_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,9 @@ az role assignment list --assignee <client-id>
# List federated credentials
az ad app federated-credential list --id <app-id>

# Test deployment (dry run)
az deployment group what-if \
--resource-group rg-whatssummarize-dev \
--template-file infra/bicep/main.bicep \
--parameters infra/parameters/dev.bicepparam
# Test deployment (dry run) — Terraform
terraform -chdir=infra/terraform/env/dev init -backend-config=backend.hcl
terraform -chdir=infra/terraform/env/dev plan
```

## Migration from Legacy Credentials
Expand Down
Loading
Loading