663 lines
22 KiB
TypeScript
663 lines
22 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { logger } from '../logger.js';
|
|
import { SourceProfileDoc, NetworkTargetDoc, RouteDoc } from '../db/index.js';
|
|
import type {
|
|
ISourceProfile,
|
|
INetworkTarget,
|
|
IRouteMetadata,
|
|
IRoute,
|
|
IRouteSecurity,
|
|
IRouteSourcePolicy,
|
|
} from '../../ts_interfaces/data/route-management.js';
|
|
|
|
const MAX_INHERITANCE_DEPTH = 5;
|
|
|
|
export class ReferenceResolver {
|
|
private profiles = new Map<string, ISourceProfile>();
|
|
private targets = new Map<string, INetworkTarget>();
|
|
|
|
// =========================================================================
|
|
// Lifecycle
|
|
// =========================================================================
|
|
|
|
public async initialize(): Promise<void> {
|
|
await this.loadProfiles();
|
|
await this.loadTargets();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Profile CRUD
|
|
// =========================================================================
|
|
|
|
public async createProfile(data: {
|
|
name: string;
|
|
description?: string;
|
|
security: IRouteSecurity;
|
|
extendsProfiles?: string[];
|
|
createdBy: string;
|
|
}): Promise<string> {
|
|
const id = plugins.uuid.v4();
|
|
const now = Date.now();
|
|
|
|
const profile: ISourceProfile = {
|
|
id,
|
|
name: data.name,
|
|
description: data.description,
|
|
security: data.security,
|
|
extendsProfiles: data.extendsProfiles,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
createdBy: data.createdBy,
|
|
};
|
|
|
|
this.profiles.set(id, profile);
|
|
await this.persistProfile(profile);
|
|
logger.log('info', `Created source profile '${profile.name}' (${id})`);
|
|
return id;
|
|
}
|
|
|
|
public async updateProfile(
|
|
id: string,
|
|
patch: Partial<Omit<ISourceProfile, 'id' | 'createdAt' | 'createdBy'>>,
|
|
): Promise<{ affectedRouteIds: string[] }> {
|
|
const profile = this.profiles.get(id);
|
|
if (!profile) {
|
|
throw new Error(`Source profile '${id}' not found`);
|
|
}
|
|
|
|
if (patch.name !== undefined) profile.name = patch.name;
|
|
if (patch.description !== undefined) profile.description = patch.description;
|
|
if (patch.security !== undefined) profile.security = patch.security;
|
|
if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles;
|
|
profile.updatedAt = Date.now();
|
|
|
|
await this.persistProfile(profile);
|
|
logger.log('info', `Updated source profile '${profile.name}' (${id})`);
|
|
|
|
// Find routes referencing this profile
|
|
const affectedRouteIds = await this.findRoutesByProfileRef(id);
|
|
return { affectedRouteIds };
|
|
}
|
|
|
|
public async deleteProfile(
|
|
id: string,
|
|
force: boolean,
|
|
storedRoutes?: Map<string, IRoute>,
|
|
): Promise<{ success: boolean; message?: string }> {
|
|
const profile = this.profiles.get(id);
|
|
if (!profile) {
|
|
return { success: false, message: `Source profile '${id}' not found` };
|
|
}
|
|
|
|
// Check usage
|
|
const affectedIds = storedRoutes
|
|
? this.findRoutesByProfileRefSync(id, storedRoutes)
|
|
: await this.findRoutesByProfileRef(id);
|
|
|
|
if (affectedIds.length > 0 && !force) {
|
|
return {
|
|
success: false,
|
|
message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
|
};
|
|
}
|
|
|
|
// Delete from DB
|
|
const doc = await SourceProfileDoc.findById(id);
|
|
if (doc) await doc.delete();
|
|
this.profiles.delete(id);
|
|
|
|
// If force-deleting with referencing routes, clear refs but keep resolved values
|
|
if (affectedIds.length > 0) {
|
|
await this.clearProfileRefsOnRoutes(id, affectedIds, storedRoutes);
|
|
logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
|
} else {
|
|
logger.log('info', `Deleted source profile '${profile.name}' (${id})`);
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
public getProfile(id: string): ISourceProfile | undefined {
|
|
return this.profiles.get(id);
|
|
}
|
|
|
|
public getProfileByName(name: string): ISourceProfile | undefined {
|
|
for (const profile of this.profiles.values()) {
|
|
if (profile.name === name) return profile;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public listProfiles(): ISourceProfile[] {
|
|
return [...this.profiles.values()];
|
|
}
|
|
|
|
public resolveSourceProfileSecurity(profileId: string): IRouteSecurity | null {
|
|
const resolvedSecurity = this.resolveSourceProfile(profileId);
|
|
return resolvedSecurity ? this.cloneSecurityFields(resolvedSecurity) : null;
|
|
}
|
|
|
|
public getProfileUsage(storedRoutes: Map<string, IRoute>): Map<string, Array<{ id: string; routeName: string }>> {
|
|
const usage = new Map<string, Array<{ id: string; routeName: string }>>();
|
|
for (const profile of this.profiles.values()) {
|
|
usage.set(profile.id, []);
|
|
}
|
|
for (const [routeId, stored] of storedRoutes) {
|
|
const refs = this.getSourceProfileRefsFromMetadata(stored.metadata);
|
|
for (const ref of refs) {
|
|
if (usage.has(ref)) {
|
|
usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
}
|
|
}
|
|
}
|
|
return usage;
|
|
}
|
|
|
|
public getProfileUsageForId(
|
|
profileId: string,
|
|
storedRoutes: Map<string, IRoute>,
|
|
): Array<{ id: string; routeName: string }> {
|
|
const routes: Array<{ id: string; routeName: string }> = [];
|
|
for (const [routeId, stored] of storedRoutes) {
|
|
if (this.metadataUsesSourceProfile(stored.metadata, profileId)) {
|
|
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
}
|
|
}
|
|
return routes;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Target CRUD
|
|
// =========================================================================
|
|
|
|
public async createTarget(data: {
|
|
name: string;
|
|
description?: string;
|
|
host: string | string[];
|
|
port: number;
|
|
createdBy: string;
|
|
}): Promise<string> {
|
|
const id = plugins.uuid.v4();
|
|
const now = Date.now();
|
|
|
|
const target: INetworkTarget = {
|
|
id,
|
|
name: data.name,
|
|
description: data.description,
|
|
host: data.host,
|
|
port: data.port,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
createdBy: data.createdBy,
|
|
};
|
|
|
|
this.targets.set(id, target);
|
|
await this.persistTarget(target);
|
|
logger.log('info', `Created network target '${target.name}' (${id})`);
|
|
return id;
|
|
}
|
|
|
|
public async updateTarget(
|
|
id: string,
|
|
patch: Partial<Omit<INetworkTarget, 'id' | 'createdAt' | 'createdBy'>>,
|
|
): Promise<{ affectedRouteIds: string[] }> {
|
|
const target = this.targets.get(id);
|
|
if (!target) {
|
|
throw new Error(`Network target '${id}' not found`);
|
|
}
|
|
|
|
if (patch.name !== undefined) target.name = patch.name;
|
|
if (patch.description !== undefined) target.description = patch.description;
|
|
if (patch.host !== undefined) target.host = patch.host;
|
|
if (patch.port !== undefined) target.port = patch.port;
|
|
target.updatedAt = Date.now();
|
|
|
|
await this.persistTarget(target);
|
|
logger.log('info', `Updated network target '${target.name}' (${id})`);
|
|
|
|
const affectedRouteIds = await this.findRoutesByTargetRef(id);
|
|
return { affectedRouteIds };
|
|
}
|
|
|
|
public async deleteTarget(
|
|
id: string,
|
|
force: boolean,
|
|
storedRoutes?: Map<string, IRoute>,
|
|
): Promise<{ success: boolean; message?: string }> {
|
|
const target = this.targets.get(id);
|
|
if (!target) {
|
|
return { success: false, message: `Network target '${id}' not found` };
|
|
}
|
|
|
|
const affectedIds = storedRoutes
|
|
? this.findRoutesByTargetRefSync(id, storedRoutes)
|
|
: await this.findRoutesByTargetRef(id);
|
|
|
|
if (affectedIds.length > 0 && !force) {
|
|
return {
|
|
success: false,
|
|
message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
|
};
|
|
}
|
|
|
|
const doc = await NetworkTargetDoc.findById(id);
|
|
if (doc) await doc.delete();
|
|
this.targets.delete(id);
|
|
|
|
if (affectedIds.length > 0) {
|
|
await this.clearTargetRefsOnRoutes(affectedIds);
|
|
logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
|
} else {
|
|
logger.log('info', `Deleted network target '${target.name}' (${id})`);
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
public getTarget(id: string): INetworkTarget | undefined {
|
|
return this.targets.get(id);
|
|
}
|
|
|
|
public getTargetByName(name: string): INetworkTarget | undefined {
|
|
for (const target of this.targets.values()) {
|
|
if (target.name === name) return target;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public listTargets(): INetworkTarget[] {
|
|
return [...this.targets.values()];
|
|
}
|
|
|
|
public getTargetUsageForId(
|
|
targetId: string,
|
|
storedRoutes: Map<string, IRoute>,
|
|
): Array<{ id: string; routeName: string }> {
|
|
const routes: Array<{ id: string; routeName: string }> = [];
|
|
for (const [routeId, stored] of storedRoutes) {
|
|
if (stored.metadata?.networkTargetRef === targetId) {
|
|
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
|
}
|
|
}
|
|
return routes;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Resolution
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Resolve references for a single route.
|
|
* Materializes source profile and/or network target into the route's fields.
|
|
* When a source profile is selected, it owns the route security fully.
|
|
* Returns the resolved route and updated metadata.
|
|
*/
|
|
public resolveRoute(
|
|
route: plugins.smartproxy.IRouteConfig,
|
|
metadata?: IRouteMetadata,
|
|
): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
|
|
const resolvedMetadata: IRouteMetadata = { ...metadata };
|
|
|
|
if (resolvedMetadata.sourcePolicy?.bindings.length) {
|
|
const resolvedSourcePolicy = this.resolveRouteSourcePolicy(resolvedMetadata.sourcePolicy);
|
|
if (resolvedSourcePolicy) {
|
|
resolvedMetadata.sourcePolicy = resolvedSourcePolicy;
|
|
resolvedMetadata.sourceProfileRef = undefined;
|
|
resolvedMetadata.sourceProfileName = undefined;
|
|
resolvedMetadata.lastResolvedAt = Date.now();
|
|
}
|
|
} else if (resolvedMetadata.sourceProfileRef) {
|
|
const resolvedSecurity = this.resolveSourceProfile(resolvedMetadata.sourceProfileRef);
|
|
if (resolvedSecurity) {
|
|
const profile = this.profiles.get(resolvedMetadata.sourceProfileRef);
|
|
route = {
|
|
...route,
|
|
security: this.cloneSecurityFields(resolvedSecurity),
|
|
};
|
|
resolvedMetadata.sourceProfileName = profile?.name;
|
|
resolvedMetadata.lastResolvedAt = Date.now();
|
|
} else {
|
|
logger.log('warn', `Source profile '${resolvedMetadata.sourceProfileRef}' not found during resolution`);
|
|
}
|
|
}
|
|
|
|
if (resolvedMetadata.networkTargetRef) {
|
|
const target = this.targets.get(resolvedMetadata.networkTargetRef);
|
|
if (target) {
|
|
const hosts = Array.isArray(target.host) ? target.host : [target.host];
|
|
route = {
|
|
...route,
|
|
action: {
|
|
...route.action,
|
|
targets: hosts.map((h) => ({
|
|
host: h,
|
|
port: target.port,
|
|
})),
|
|
},
|
|
};
|
|
resolvedMetadata.networkTargetName = target.name;
|
|
resolvedMetadata.lastResolvedAt = Date.now();
|
|
} else {
|
|
logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`);
|
|
}
|
|
}
|
|
|
|
return { route, metadata: resolvedMetadata };
|
|
}
|
|
|
|
// =========================================================================
|
|
// Reference lookup helpers
|
|
// =========================================================================
|
|
|
|
public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
|
|
const docs = await RouteDoc.findAll();
|
|
return docs
|
|
.filter((doc) => this.metadataUsesSourceProfile(doc.metadata, profileId))
|
|
.map((doc) => doc.id);
|
|
}
|
|
|
|
public async findRoutesByTargetRef(targetId: string): Promise<string[]> {
|
|
const docs = await RouteDoc.findAll();
|
|
return docs
|
|
.filter((doc) => doc.metadata?.networkTargetRef === targetId)
|
|
.map((doc) => doc.id);
|
|
}
|
|
|
|
public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IRoute>): string[] {
|
|
const ids: string[] = [];
|
|
for (const [routeId, stored] of storedRoutes) {
|
|
if (this.metadataUsesSourceProfile(stored.metadata, profileId)) {
|
|
ids.push(routeId);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map<string, IRoute>): string[] {
|
|
const ids: string[] = [];
|
|
for (const [routeId, stored] of storedRoutes) {
|
|
if (stored.metadata?.networkTargetRef === targetId) {
|
|
ids.push(routeId);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Private: source profile resolution with inheritance
|
|
// =========================================================================
|
|
|
|
private resolveRouteSourcePolicy(sourcePolicy: IRouteSourcePolicy): IRouteSourcePolicy | undefined {
|
|
const bindings = sourcePolicy.bindings
|
|
.map((binding) => {
|
|
const profile = this.profiles.get(binding.sourceProfileRef);
|
|
if (!profile) {
|
|
logger.log('warn', `Source profile '${binding.sourceProfileRef}' not found during source policy resolution`);
|
|
return binding;
|
|
}
|
|
return {
|
|
...binding,
|
|
sourceProfileName: profile.name,
|
|
};
|
|
})
|
|
.filter((binding) => binding.sourceProfileRef);
|
|
|
|
return bindings.length > 0 ? { bindings } : undefined;
|
|
}
|
|
|
|
private metadataUsesSourceProfile(metadata: IRouteMetadata | undefined, profileId: string): boolean {
|
|
return this.getSourceProfileRefsFromMetadata(metadata).includes(profileId);
|
|
}
|
|
|
|
private getSourceProfileRefsFromMetadata(metadata: IRouteMetadata | undefined): string[] {
|
|
const refs = new Set<string>();
|
|
if (metadata?.sourceProfileRef) {
|
|
refs.add(metadata.sourceProfileRef);
|
|
}
|
|
for (const binding of metadata?.sourcePolicy?.bindings || []) {
|
|
if (binding.sourceProfileRef) {
|
|
refs.add(binding.sourceProfileRef);
|
|
}
|
|
}
|
|
return [...refs];
|
|
}
|
|
|
|
private resolveSourceProfile(
|
|
profileId: string,
|
|
visited: Set<string> = new Set(),
|
|
depth: number = 0,
|
|
): IRouteSecurity | null {
|
|
if (depth > MAX_INHERITANCE_DEPTH) {
|
|
logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`);
|
|
return null;
|
|
}
|
|
|
|
if (visited.has(profileId)) {
|
|
logger.log('warn', `Circular inheritance detected for profile '${profileId}'`);
|
|
return null;
|
|
}
|
|
|
|
const profile = this.profiles.get(profileId);
|
|
if (!profile) return null;
|
|
|
|
visited.add(profileId);
|
|
|
|
// Start with an empty base
|
|
let baseSecurity: IRouteSecurity = {};
|
|
|
|
// Resolve parent profiles first (top-down, later overrides earlier)
|
|
if (profile.extendsProfiles?.length) {
|
|
for (const parentId of profile.extendsProfiles) {
|
|
const parentSecurity = this.resolveSourceProfile(parentId, new Set(visited), depth + 1);
|
|
if (parentSecurity) {
|
|
baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply this profile's security on top
|
|
return this.mergeSecurityFields(baseSecurity, profile.security);
|
|
}
|
|
|
|
/**
|
|
* Merge two IRouteSecurity objects.
|
|
* `override` values take precedence over `base` values.
|
|
* For ipAllowList/ipBlockList: union arrays and deduplicate.
|
|
* For scalar/object fields: override wins if present.
|
|
*/
|
|
private mergeSecurityFields(
|
|
base: IRouteSecurity | undefined,
|
|
override: IRouteSecurity | undefined,
|
|
): IRouteSecurity {
|
|
if (!base && !override) return {};
|
|
if (!base) return { ...override };
|
|
if (!override) return { ...base };
|
|
|
|
const merged: IRouteSecurity = { ...base };
|
|
|
|
// IP lists: union
|
|
if (override.ipAllowList || base.ipAllowList) {
|
|
merged.ipAllowList = [...new Set([
|
|
...(base.ipAllowList || []),
|
|
...(override.ipAllowList || []),
|
|
])];
|
|
}
|
|
|
|
if (override.ipBlockList || base.ipBlockList) {
|
|
merged.ipBlockList = [...new Set([
|
|
...(base.ipBlockList || []),
|
|
...(override.ipBlockList || []),
|
|
])];
|
|
}
|
|
|
|
// Scalar/object fields: override wins
|
|
if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections;
|
|
if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit;
|
|
if (override.authentication !== undefined) merged.authentication = override.authentication;
|
|
if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
|
|
if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
|
|
if (override.vpn !== undefined) merged.vpn = override.vpn;
|
|
|
|
return merged;
|
|
}
|
|
|
|
private cloneSecurityFields(security: IRouteSecurity): IRouteSecurity {
|
|
return structuredClone(security);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Private: persistence
|
|
// =========================================================================
|
|
|
|
private async loadProfiles(): Promise<void> {
|
|
const docs = await SourceProfileDoc.findAll();
|
|
for (const doc of docs) {
|
|
if (doc.id) {
|
|
this.profiles.set(doc.id, {
|
|
id: doc.id,
|
|
name: doc.name,
|
|
description: doc.description,
|
|
security: doc.security,
|
|
extendsProfiles: doc.extendsProfiles,
|
|
createdAt: doc.createdAt,
|
|
updatedAt: doc.updatedAt,
|
|
createdBy: doc.createdBy,
|
|
});
|
|
}
|
|
}
|
|
if (this.profiles.size > 0) {
|
|
logger.log('info', `Loaded ${this.profiles.size} source profile(s) from storage`);
|
|
}
|
|
}
|
|
|
|
private async loadTargets(): Promise<void> {
|
|
const docs = await NetworkTargetDoc.findAll();
|
|
for (const doc of docs) {
|
|
if (doc.id) {
|
|
this.targets.set(doc.id, {
|
|
id: doc.id,
|
|
name: doc.name,
|
|
description: doc.description,
|
|
host: doc.host,
|
|
port: doc.port,
|
|
createdAt: doc.createdAt,
|
|
updatedAt: doc.updatedAt,
|
|
createdBy: doc.createdBy,
|
|
});
|
|
}
|
|
}
|
|
if (this.targets.size > 0) {
|
|
logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`);
|
|
}
|
|
}
|
|
|
|
private async persistProfile(profile: ISourceProfile): Promise<void> {
|
|
const existingDoc = await SourceProfileDoc.findById(profile.id);
|
|
if (existingDoc) {
|
|
existingDoc.name = profile.name;
|
|
existingDoc.description = profile.description;
|
|
existingDoc.security = profile.security;
|
|
existingDoc.extendsProfiles = profile.extendsProfiles;
|
|
existingDoc.updatedAt = profile.updatedAt;
|
|
await existingDoc.save();
|
|
} else {
|
|
const doc = new SourceProfileDoc();
|
|
doc.id = profile.id;
|
|
doc.name = profile.name;
|
|
doc.description = profile.description;
|
|
doc.security = profile.security;
|
|
doc.extendsProfiles = profile.extendsProfiles;
|
|
doc.createdAt = profile.createdAt;
|
|
doc.updatedAt = profile.updatedAt;
|
|
doc.createdBy = profile.createdBy;
|
|
await doc.save();
|
|
}
|
|
}
|
|
|
|
private async persistTarget(target: INetworkTarget): Promise<void> {
|
|
const existingDoc = await NetworkTargetDoc.findById(target.id);
|
|
if (existingDoc) {
|
|
existingDoc.name = target.name;
|
|
existingDoc.description = target.description;
|
|
existingDoc.host = target.host;
|
|
existingDoc.port = target.port;
|
|
existingDoc.updatedAt = target.updatedAt;
|
|
await existingDoc.save();
|
|
} else {
|
|
const doc = new NetworkTargetDoc();
|
|
doc.id = target.id;
|
|
doc.name = target.name;
|
|
doc.description = target.description;
|
|
doc.host = target.host;
|
|
doc.port = target.port;
|
|
doc.createdAt = target.createdAt;
|
|
doc.updatedAt = target.updatedAt;
|
|
doc.createdBy = target.createdBy;
|
|
await doc.save();
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Private: ref cleanup on force-delete
|
|
// =========================================================================
|
|
|
|
private async clearProfileRefsOnRoutes(
|
|
profileId: string,
|
|
routeIds: string[],
|
|
storedRoutes?: Map<string, IRoute>,
|
|
): Promise<void> {
|
|
for (const routeId of routeIds) {
|
|
const doc = await RouteDoc.findById(routeId);
|
|
if (doc?.metadata) {
|
|
doc.metadata = this.clearSourceProfileFromMetadata(doc.metadata, profileId);
|
|
doc.updatedAt = Date.now();
|
|
await doc.save();
|
|
}
|
|
|
|
const storedRoute = storedRoutes?.get(routeId);
|
|
if (storedRoute?.metadata) {
|
|
storedRoute.metadata = this.clearSourceProfileFromMetadata(storedRoute.metadata, profileId);
|
|
storedRoute.updatedAt = Date.now();
|
|
}
|
|
}
|
|
}
|
|
|
|
private clearSourceProfileFromMetadata(metadata: IRouteMetadata, profileId: string): IRouteMetadata {
|
|
const sourcePolicy = metadata.sourcePolicy?.bindings?.length
|
|
? {
|
|
bindings: metadata.sourcePolicy.bindings.filter(
|
|
(binding) => binding.sourceProfileRef !== profileId,
|
|
),
|
|
}
|
|
: undefined;
|
|
|
|
const nextMetadata: IRouteMetadata = {
|
|
...metadata,
|
|
sourceProfileRef: metadata.sourceProfileRef === profileId ? undefined : metadata.sourceProfileRef,
|
|
sourceProfileName: metadata.sourceProfileRef === profileId ? undefined : metadata.sourceProfileName,
|
|
sourcePolicy: sourcePolicy?.bindings.length ? sourcePolicy : undefined,
|
|
};
|
|
|
|
if (!nextMetadata.sourceProfileRef && !nextMetadata.sourcePolicy && !nextMetadata.networkTargetRef) {
|
|
nextMetadata.lastResolvedAt = undefined;
|
|
}
|
|
|
|
return nextMetadata;
|
|
}
|
|
|
|
private async clearTargetRefsOnRoutes(routeIds: string[]): Promise<void> {
|
|
for (const routeId of routeIds) {
|
|
const doc = await RouteDoc.findById(routeId);
|
|
if (doc?.metadata) {
|
|
doc.metadata = {
|
|
...doc.metadata,
|
|
networkTargetRef: undefined,
|
|
networkTargetName: undefined,
|
|
};
|
|
doc.updatedAt = Date.now();
|
|
await doc.save();
|
|
}
|
|
}
|
|
}
|
|
}
|