You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

340 lines
10 KiB

/**
* DeepLinks Service
*
* Handles deep link processing and routing for the TimeSafari application.
* Supports both path parameters and query parameters with comprehensive validation.
*
* @author Matthew Raymer
* @version 2.0.0
* @since 2025-01-25
*/
import { Router } from "vue-router";
import { z } from "zod";
import {
deepLinkPathSchemas,
routeSchema,
DeepLinkRoute,
deepLinkQuerySchemas,
} from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger";
// Helper function to extract the first key from a Zod object schema
function getFirstKeyFromZodObject(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema: z.ZodObject<any>,
): string | undefined {
const shape = schema.shape;
const keys = Object.keys(shape);
return keys.length > 0 ? keys[0] : undefined;
}
/**
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
*
* It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'.
*
* The paramKey is used to extract the parameter from the route path,
* because "router.replace" expects the right parameter name for the route.
*/
export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> =
Object.entries(deepLinkPathSchemas).reduce(
(acc, [routeName, schema]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
acc[routeName] = {
name: routeName,
paramKey,
};
return acc;
},
{} as Record<string, { name: string; paramKey?: string }>,
);
/**
* Handles processing and routing of deep links in the application.
* Provides validation, error handling, and routing for deep link URLs.
*/
export class DeepLinkHandler {
private router: Router;
/**
* Creates a new DeepLinkHandler instance.
* @param router - Vue Router instance for navigation
*/
constructor(router: Router) {
this.router = router;
}
/**
* Main entry point for processing deep links
* @param url - The deep link URL to process
* @throws {DeepLinkError} If validation fails or route is invalid
*/
async handleDeepLink(url: string): Promise<void> {
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
try {
logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
const { path, params, query } = this.parseDeepLink(url);
logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
path,
params: Object.keys(params),
query: Object.keys(query),
fullParams: params,
fullQuery: query,
});
// Sanitize parameters (remove undefined values)
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
await this.validateAndRoute(path, sanitizedParams, query);
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
const deepLinkError = error as DeepLinkError;
throw deepLinkError;
}
}
/**
* Parse a deep link URL into its components
* @param url - The deep link URL
* @returns Parsed components
*/
private parseDeepLink(url: string): {
path: string;
params: Record<string, string>;
query: Record<string, string>;
} {
logger.debug(`[DeepLink] 🔍 Parsing deep link: ${url}`);
try {
const parts = url.split("://");
if (parts.length !== 2) {
throw new Error("Invalid URL format");
}
const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/");
// Parse path parameters
const params: Record<string, string> = {};
if (pathParams.length > 0) {
params.id = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: id=${pathParams[0]}`,
);
}
// Parse query parameters
const query: Record<string, string> = {};
if (queryString) {
const queryParams = new URLSearchParams(queryString);
for (const [key, value] of queryParams.entries()) {
query[key] = value;
}
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
}
logger.info(`[DeepLink] ✅ Parse completed:`, {
routePath,
pathParams: pathParams.length,
queryParams: Object.keys(query).length,
});
return { path: routePath, params, query };
} catch (error) {
logger.error(`[DeepLink] ❌ Parse failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Validate and route the deep link
* @param path - The route path
* @param params - Path parameters
* @param query - Query parameters
*/
private async validateAndRoute(
path: string,
params: Record<string, string>,
query: Record<string, string>,
): Promise<void> {
logger.info(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
);
// First try to validate the route path
let routeName: string;
try {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration
const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
throw new Error(`Route configuration missing for: ${validRoute}`);
}
routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) {
logger.error(`[DeepLink] ❌ Route validation failed:`, {
path,
error: error instanceof Error ? error.message : String(error),
});
// Redirect to error page with information about the invalid link
await this.router.replace({
name: "deep-link-error",
params,
query: {
originalPath: path,
errorCode: "INVALID_ROUTE",
errorMessage: `The link you followed (${path}) is not supported`,
...query,
},
});
logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
);
return;
}
// Continue with parameter validation
logger.info(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
);
const pathSchema =
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
const querySchema =
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
logger.debug(`[DeepLink] 📋 Schemas found:`, {
hasPathSchema: !!pathSchema,
hasQuerySchema: !!querySchema,
pathSchemaType: pathSchema ? typeof pathSchema : "none",
querySchemaType: querySchema ? typeof querySchema : "none",
});
let validatedPathParams: Record<string, string> = {};
let validatedQueryParams: Record<string, string> = {};
try {
if (pathSchema) {
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
validatedPathParams = await pathSchema.parseAsync(params);
logger.info(
`[DeepLink] ✅ Path parameters validated:`,
validatedPathParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No path schema found for: ${path}`);
validatedPathParams = params;
}
if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query);
logger.info(
`[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No query schema found for: ${path}`);
validatedQueryParams = query;
}
} catch (error) {
logger.error(`[DeepLink] ❌ Parameter validation failed:`, {
routeName,
path,
params,
query,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
await this.router.replace({
name: "deep-link-error",
params,
query: {
originalPath: path,
errorCode: "INVALID_PARAMETERS",
errorMessage: `The link parameters are invalid: ${(error as Error).message}`,
...query,
},
});
logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
);
return;
}
// Attempt navigation
try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
routeName,
pathParams: validatedPathParams,
queryParams: validatedQueryParams,
});
await this.router.replace({
name: routeName,
params: validatedPathParams,
query: validatedQueryParams,
});
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) {
logger.error(`[DeepLink] ❌ Navigation failed:`, {
routeName,
path,
validatedPathParams,
validatedQueryParams,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
// Redirect to error page for navigation failures
await this.router.replace({
name: "deep-link-error",
params: validatedPathParams,
query: {
originalPath: path,
errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${(error as Error).message}`,
...validatedQueryParams,
},
});
logger.info(
`[DeepLink] 🔄 Redirected to error page for navigation failure`,
);
}
}
}