refactor: reorganize deep linking types and interfaces

Changes:
- Move deep link types from types/ to interfaces/
- Export baseUrlSchema for external use
- Add trailing commas for better git diffs
- Fix type inference for deepLinkSchemas
- Add deepLinks export to interfaces/index.ts
- Remove duplicate SuccessResult interface
- Update import paths in services/deepLinks.ts

This improves code organization by centralizing interface definitions
and fixing type inference issues.
This commit is contained in:
Matthew Raymer
2025-02-26 10:28:55 +00:00
parent 9d04db4a71
commit 20620c3aae
10 changed files with 1110 additions and 158 deletions

View File

@@ -1,41 +1,37 @@
/**
* @file Deep Link Handler Service
* @author Matthew Raymer
*
*
* This service handles the processing and routing of deep links in the TimeSafari app.
* It provides a type-safe interface between the raw deep links and the application router.
*
*
* Architecture:
* 1. DeepLinkHandler class encapsulates all deep link processing logic
* 2. Uses Zod schemas from types/deepLinks for parameter validation
* 3. Provides consistent error handling and logging
* 4. Maps validated parameters to Vue router calls
*
*
* Error Handling Strategy:
* - All errors are wrapped in DeepLinkError interface
* - Errors include error codes for systematic handling
* - Detailed error information is logged for debugging
* - Errors are propagated to the global error handler
*
*
* Validation Strategy:
* - URL structure validation
* - Route-specific parameter validation using Zod schemas
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
*
*
* @example
* const handler = new DeepLinkHandler(router);
* await handler.handleDeepLink("timesafari://claim/123?view=details");
*/
import { Router } from "vue-router";
import { deepLinkSchemas, DeepLinkParams } from "../types/deepLinks";
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
import { logConsoleAndDb } from "../db";
interface DeepLinkError extends Error {
code: string;
details?: unknown;
}
import type { DeepLinkError } from "../interfaces/deepLinks";
export class DeepLinkHandler {
private router: Router;
@@ -44,6 +40,39 @@ export class DeepLinkHandler {
this.router = router;
}
/**
* Parses deep link URL into path, params and query components
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" };
}
// Validate base URL structure
baseUrlSchema.parse({
scheme: parts[0],
path: parts[1],
queryParams: {}, // Will be populated below
});
const [path, queryString] = parts[1].split("?");
const [routePath, param] = path.split("/");
const query: Record<string, string> = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
query[key] = value;
});
}
return {
path: routePath,
params: param ? { id: param } : {},
query,
};
}
/**
* Processes incoming deep links and routes them appropriately
* @param url The deep link URL to process
@@ -51,21 +80,23 @@ export class DeepLinkHandler {
async handleDeepLink(url: string): Promise<void> {
try {
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
const { path, params, query } = this.parseDeepLink(url);
await this.validateAndRoute(path, params, query);
// Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
true
true,
);
throw {
code: deepLinkError.code || 'UNKNOWN_ERROR',
code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message,
details: deepLinkError.details
details: deepLinkError.details,
};
}
}
@@ -76,25 +107,25 @@ export class DeepLinkHandler {
private async validateAndRoute(
path: string,
params: Record<string, string>,
query: Record<string, string>
query: Record<string, string>,
): Promise<void> {
const routeMap: Record<string, string> = {
claim: 'claim',
'claim-cert': 'claim-cert',
'claim-add-raw': 'claim-add-raw',
'contact-edit': 'contact-edit',
'contact-import': 'contact-import',
project: 'project',
'invite-one-accept': 'invite-one-accept',
'offer-details': 'offer-details',
'confirm-gift': 'confirm-gift'
claim: "claim",
"claim-cert": "claim-cert",
"claim-add-raw": "claim-add-raw",
"contact-edit": "contact-edit",
"contact-import": "contact-import",
project: "project",
"invite-one-accept": "invite-one-accept",
"offer-details": "offer-details",
"confirm-gift": "confirm-gift",
};
const routeName = routeMap[path];
if (!routeName) {
throw {
code: 'INVALID_ROUTE',
message: `Unsupported route: ${path}`
code: "INVALID_ROUTE",
message: `Unsupported route: ${path}`,
};
}
@@ -102,13 +133,13 @@ export class DeepLinkHandler {
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
const validatedParams = await schema.parseAsync({
...params,
...query
...query,
});
await this.router.replace({
name: routeName,
params: validatedParams,
query
query,
});
}
}
}