- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
235 lines
7.3 KiB
TypeScript
235 lines
7.3 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
ForbiddenException,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaicstack/db';
|
|
import { DB } from '../../database/database.module.js';
|
|
import { AuthGuard } from '../../auth/auth.guard.js';
|
|
import { CurrentUser } from '../../auth/current-user.decorator.js';
|
|
import {
|
|
CreateRoutingRuleDto,
|
|
UpdateRoutingRuleDto,
|
|
ReorderRoutingRulesDto,
|
|
} from './routing.dto.js';
|
|
|
|
@Controller('api/routing/rules')
|
|
@UseGuards(AuthGuard)
|
|
export class RoutingController {
|
|
constructor(@Inject(DB) private readonly db: Db) {}
|
|
|
|
/**
|
|
* GET /api/routing/rules
|
|
* List all rules visible to the authenticated user:
|
|
* - All system rules
|
|
* - User's own rules
|
|
* Ordered by priority ascending (lower number = higher priority).
|
|
*/
|
|
@Get()
|
|
async list(@CurrentUser() user: { id: string }) {
|
|
const rows = await this.db
|
|
.select()
|
|
.from(routingRules)
|
|
.where(
|
|
or(
|
|
eq(routingRules.scope, 'system'),
|
|
and(eq(routingRules.scope, 'user'), eq(routingRules.userId, user.id)),
|
|
),
|
|
)
|
|
.orderBy(asc(routingRules.priority));
|
|
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* GET /api/routing/rules/effective
|
|
* Return the merged rule set in priority order.
|
|
* User-scoped rules are checked before system rules at the same priority
|
|
* (achieved by ordering: priority ASC, then scope='user' first).
|
|
*/
|
|
@Get('effective')
|
|
async effective(@CurrentUser() user: { id: string }) {
|
|
const rows = await this.db
|
|
.select()
|
|
.from(routingRules)
|
|
.where(
|
|
and(
|
|
eq(routingRules.enabled, true),
|
|
or(
|
|
eq(routingRules.scope, 'system'),
|
|
and(eq(routingRules.scope, 'user'), eq(routingRules.userId, user.id)),
|
|
),
|
|
),
|
|
)
|
|
.orderBy(asc(routingRules.priority));
|
|
|
|
// For rules with the same priority: user rules beat system rules.
|
|
// Group by priority then stable-sort each group: user before system.
|
|
const grouped = new Map<number, typeof rows>();
|
|
for (const row of rows) {
|
|
const bucket = grouped.get(row.priority) ?? [];
|
|
bucket.push(row);
|
|
grouped.set(row.priority, bucket);
|
|
}
|
|
|
|
const effective: typeof rows = [];
|
|
for (const [, bucket] of [...grouped.entries()].sort(([a], [b]) => a - b)) {
|
|
// user-scoped rules first within the same priority bucket
|
|
const userRules = bucket.filter((r) => r.scope === 'user');
|
|
const systemRules = bucket.filter((r) => r.scope === 'system');
|
|
effective.push(...userRules, ...systemRules);
|
|
}
|
|
|
|
return effective;
|
|
}
|
|
|
|
/**
|
|
* POST /api/routing/rules
|
|
* Create a new routing rule. Scope is forced to 'user' (users cannot create
|
|
* system rules). The authenticated user's ID is attached automatically.
|
|
*/
|
|
@Post()
|
|
async create(@Body() dto: CreateRoutingRuleDto, @CurrentUser() user: { id: string }) {
|
|
const [created] = await this.db
|
|
.insert(routingRules)
|
|
.values({
|
|
name: dto.name,
|
|
priority: dto.priority,
|
|
scope: 'user',
|
|
userId: user.id,
|
|
conditions: dto.conditions as unknown as Record<string, unknown>[],
|
|
action: dto.action as unknown as Record<string, unknown>,
|
|
enabled: dto.enabled ?? true,
|
|
})
|
|
.returning();
|
|
|
|
return created;
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/routing/rules/reorder
|
|
* Reassign priorities so that the order of `ruleIds` reflects ascending
|
|
* priority (index 0 = priority 0, index 1 = priority 1, …).
|
|
* Only the authenticated user's own rules can be reordered.
|
|
*/
|
|
@Patch('reorder')
|
|
async reorder(@Body() dto: ReorderRoutingRulesDto, @CurrentUser() user: { id: string }) {
|
|
// Verify all supplied IDs belong to this user
|
|
const owned = await this.db
|
|
.select({ id: routingRules.id })
|
|
.from(routingRules)
|
|
.where(
|
|
and(
|
|
inArray(routingRules.id, dto.ruleIds),
|
|
eq(routingRules.scope, 'user'),
|
|
eq(routingRules.userId, user.id),
|
|
),
|
|
);
|
|
|
|
const ownedIds = new Set(owned.map((r) => r.id));
|
|
const unowned = dto.ruleIds.filter((id) => !ownedIds.has(id));
|
|
if (unowned.length > 0) {
|
|
throw new ForbiddenException(
|
|
`Cannot reorder rules that do not belong to you: ${unowned.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
// Apply new priorities in transaction
|
|
const updates = await this.db.transaction(async (tx) => {
|
|
const results = [];
|
|
for (let i = 0; i < dto.ruleIds.length; i++) {
|
|
const [updated] = await tx
|
|
.update(routingRules)
|
|
.set({ priority: i, updatedAt: new Date() })
|
|
.where(and(eq(routingRules.id, dto.ruleIds[i]!), eq(routingRules.userId, user.id)))
|
|
.returning();
|
|
if (updated) results.push(updated);
|
|
}
|
|
return results;
|
|
});
|
|
|
|
return updates;
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/routing/rules/:id
|
|
* Update a user-owned rule. System rules cannot be modified by regular users.
|
|
*/
|
|
@Patch(':id')
|
|
async update(
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateRoutingRuleDto,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const [existing] = await this.db.select().from(routingRules).where(eq(routingRules.id, id));
|
|
|
|
if (!existing) throw new NotFoundException('Routing rule not found');
|
|
|
|
if (existing.scope === 'system') {
|
|
throw new ForbiddenException('System routing rules cannot be modified');
|
|
}
|
|
|
|
if (existing.userId !== user.id) {
|
|
throw new ForbiddenException('Routing rule does not belong to the current user');
|
|
}
|
|
|
|
const updatePayload: Partial<typeof routingRules.$inferInsert> = {
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (dto.name !== undefined) updatePayload.name = dto.name;
|
|
if (dto.priority !== undefined) updatePayload.priority = dto.priority;
|
|
if (dto.conditions !== undefined)
|
|
updatePayload.conditions = dto.conditions as unknown as Record<string, unknown>[];
|
|
if (dto.action !== undefined)
|
|
updatePayload.action = dto.action as unknown as Record<string, unknown>;
|
|
if (dto.enabled !== undefined) updatePayload.enabled = dto.enabled;
|
|
|
|
const [updated] = await this.db
|
|
.update(routingRules)
|
|
.set(updatePayload)
|
|
.where(and(eq(routingRules.id, id), eq(routingRules.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!updated) throw new NotFoundException('Routing rule not found');
|
|
return updated;
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/routing/rules/:id
|
|
* Delete a user-owned routing rule. System rules cannot be deleted.
|
|
*/
|
|
@Delete(':id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
|
const [existing] = await this.db.select().from(routingRules).where(eq(routingRules.id, id));
|
|
|
|
if (!existing) throw new NotFoundException('Routing rule not found');
|
|
|
|
if (existing.scope === 'system') {
|
|
throw new ForbiddenException('System routing rules cannot be deleted');
|
|
}
|
|
|
|
if (existing.userId !== user.id) {
|
|
throw new ForbiddenException('Routing rule does not belong to the current user');
|
|
}
|
|
|
|
const [deleted] = await this.db
|
|
.delete(routingRules)
|
|
.where(and(eq(routingRules.id, id), eq(routingRules.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!deleted) throw new NotFoundException('Routing rule not found');
|
|
}
|
|
}
|