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.
350 lines
11 KiB
350 lines
11 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 using route-specific configuration
|
|
const params: Record<string, string> = {};
|
|
if (pathParams.length > 0) {
|
|
// Get the correct parameter key for this route
|
|
const routeConfig = ROUTE_MAP[routePath];
|
|
if (routeConfig?.paramKey) {
|
|
params[routeConfig.paramKey] = pathParams[0];
|
|
logger.debug(
|
|
`[DeepLink] 📍 Path parameter extracted: ${routeConfig.paramKey}=${pathParams[0]}`,
|
|
);
|
|
} else {
|
|
// Fallback to 'id' for backward compatibility
|
|
params.id = pathParams[0];
|
|
logger.debug(
|
|
`[DeepLink] 📍 Path parameter extracted: id=${pathParams[0]} (fallback)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|