@htlkg/core 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.js +121 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -8
- package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
- package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
- package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
- package/src/amplify-astro-adapter/errors.test.ts +115 -0
- package/src/amplify-astro-adapter/errors.ts +105 -0
- package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
- package/src/amplify-astro-adapter/globalSettings.ts +16 -0
- package/src/amplify-astro-adapter/index.ts +14 -0
- package/src/amplify-astro-adapter/types.ts +55 -0
- package/src/auth/auth.md +178 -0
- package/src/auth/index.test.ts +180 -0
- package/src/auth/index.ts +294 -0
- package/src/constants/constants.md +132 -0
- package/src/constants/index.test.ts +116 -0
- package/src/constants/index.ts +98 -0
- package/src/core-exports.property.test.ts +186 -0
- package/src/errors/errors.md +177 -0
- package/src/errors/index.test.ts +153 -0
- package/src/errors/index.ts +134 -0
- package/src/index.ts +65 -0
- package/src/routes/index.ts +225 -0
- package/src/routes/routes.md +189 -0
- package/src/types/index.ts +94 -0
- package/src/types/types.md +144 -0
- package/src/utils/index.test.ts +257 -0
- package/src/utils/index.ts +112 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/utils.md +199 -0
- package/src/workspace.property.test.ts +235 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AppError,
|
|
4
|
+
AuthError,
|
|
5
|
+
ValidationError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
} from "./index";
|
|
8
|
+
|
|
9
|
+
describe("Error Classes", () => {
|
|
10
|
+
describe("AppError", () => {
|
|
11
|
+
it("should create error with default values", () => {
|
|
12
|
+
const error = new AppError("Test error");
|
|
13
|
+
|
|
14
|
+
expect(error.message).toBe("Test error");
|
|
15
|
+
expect(error.code).toBe("APP_ERROR");
|
|
16
|
+
expect(error.statusCode).toBe(500);
|
|
17
|
+
expect(error.name).toBe("AppError");
|
|
18
|
+
expect(error.details).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should create error with custom values", () => {
|
|
22
|
+
const details = { userId: "123" };
|
|
23
|
+
const error = new AppError("Custom error", "CUSTOM_CODE", 400, details);
|
|
24
|
+
|
|
25
|
+
expect(error.message).toBe("Custom error");
|
|
26
|
+
expect(error.code).toBe("CUSTOM_CODE");
|
|
27
|
+
expect(error.statusCode).toBe(400);
|
|
28
|
+
expect(error.details).toEqual(details);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should be instanceof Error", () => {
|
|
32
|
+
const error = new AppError("Test");
|
|
33
|
+
expect(error).toBeInstanceOf(Error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should convert to JSON", () => {
|
|
37
|
+
const error = new AppError("Test", "CODE", 400, { key: "value" });
|
|
38
|
+
const json = error.toJSON();
|
|
39
|
+
|
|
40
|
+
expect(json).toEqual({
|
|
41
|
+
name: "AppError",
|
|
42
|
+
message: "Test",
|
|
43
|
+
code: "CODE",
|
|
44
|
+
statusCode: 400,
|
|
45
|
+
details: { key: "value" },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("AuthError", () => {
|
|
51
|
+
it("should create auth error with default values", () => {
|
|
52
|
+
const error = new AuthError("Unauthorized");
|
|
53
|
+
|
|
54
|
+
expect(error.message).toBe("Unauthorized");
|
|
55
|
+
expect(error.code).toBe("AUTH_ERROR");
|
|
56
|
+
expect(error.statusCode).toBe(401);
|
|
57
|
+
expect(error.name).toBe("AuthError");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should create auth error with custom values", () => {
|
|
61
|
+
const error = new AuthError("Invalid token", "INVALID_TOKEN", 403);
|
|
62
|
+
|
|
63
|
+
expect(error.message).toBe("Invalid token");
|
|
64
|
+
expect(error.code).toBe("INVALID_TOKEN");
|
|
65
|
+
expect(error.statusCode).toBe(403);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should be instanceof AppError", () => {
|
|
69
|
+
const error = new AuthError("Test");
|
|
70
|
+
expect(error).toBeInstanceOf(AppError);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("ValidationError", () => {
|
|
75
|
+
it("should create validation error with default values", () => {
|
|
76
|
+
const error = new ValidationError("Validation failed");
|
|
77
|
+
|
|
78
|
+
expect(error.message).toBe("Validation failed");
|
|
79
|
+
expect(error.code).toBe("VALIDATION_ERROR");
|
|
80
|
+
expect(error.statusCode).toBe(400);
|
|
81
|
+
expect(error.name).toBe("ValidationError");
|
|
82
|
+
expect(error.errors).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should create validation error with field errors", () => {
|
|
86
|
+
const fieldErrors = [
|
|
87
|
+
{ field: "email", message: "Invalid email" },
|
|
88
|
+
{ field: "password", message: "Too short" },
|
|
89
|
+
];
|
|
90
|
+
const error = new ValidationError("Validation failed", fieldErrors);
|
|
91
|
+
|
|
92
|
+
expect(error.errors).toEqual(fieldErrors);
|
|
93
|
+
expect(error.details).toEqual({ errors: fieldErrors });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should convert to JSON with errors", () => {
|
|
97
|
+
const fieldErrors = [{ field: "email", message: "Invalid" }];
|
|
98
|
+
const error = new ValidationError("Failed", fieldErrors);
|
|
99
|
+
const json = error.toJSON();
|
|
100
|
+
|
|
101
|
+
expect(json.errors).toEqual(fieldErrors);
|
|
102
|
+
expect(json.name).toBe("ValidationError");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should be instanceof AppError", () => {
|
|
106
|
+
const error = new ValidationError("Test");
|
|
107
|
+
expect(error).toBeInstanceOf(AppError);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("NotFoundError", () => {
|
|
112
|
+
it("should create not found error with resource only", () => {
|
|
113
|
+
const error = new NotFoundError("User");
|
|
114
|
+
|
|
115
|
+
expect(error.message).toBe("User not found");
|
|
116
|
+
expect(error.code).toBe("NOT_FOUND");
|
|
117
|
+
expect(error.statusCode).toBe(404);
|
|
118
|
+
expect(error.name).toBe("NotFoundError");
|
|
119
|
+
expect(error.resource).toBe("User");
|
|
120
|
+
expect(error.resourceId).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should create not found error with resource and ID", () => {
|
|
124
|
+
const error = new NotFoundError("User", "123");
|
|
125
|
+
|
|
126
|
+
expect(error.message).toBe("User with id '123' not found");
|
|
127
|
+
expect(error.resource).toBe("User");
|
|
128
|
+
expect(error.resourceId).toBe("123");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should create not found error with custom message", () => {
|
|
132
|
+
const error = new NotFoundError("User", "123", "Custom not found message");
|
|
133
|
+
|
|
134
|
+
expect(error.message).toBe("Custom not found message");
|
|
135
|
+
expect(error.resource).toBe("User");
|
|
136
|
+
expect(error.resourceId).toBe("123");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should convert to JSON with resource info", () => {
|
|
140
|
+
const error = new NotFoundError("Brand", "456");
|
|
141
|
+
const json = error.toJSON();
|
|
142
|
+
|
|
143
|
+
expect(json.resource).toBe("Brand");
|
|
144
|
+
expect(json.resourceId).toBe("456");
|
|
145
|
+
expect(json.name).toBe("NotFoundError");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should be instanceof AppError", () => {
|
|
149
|
+
const error = new NotFoundError("Test");
|
|
150
|
+
expect(error).toBeInstanceOf(AppError);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @htlkg/core/errors
|
|
3
|
+
* Core error classes for Hotelinking applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base application error class
|
|
8
|
+
*/
|
|
9
|
+
export class AppError extends Error {
|
|
10
|
+
public readonly code: string;
|
|
11
|
+
public readonly statusCode: number;
|
|
12
|
+
public readonly details?: Record<string, any>;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
message: string,
|
|
16
|
+
code = "APP_ERROR",
|
|
17
|
+
statusCode = 500,
|
|
18
|
+
details?: Record<string, any>,
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "AppError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
this.details = details;
|
|
25
|
+
|
|
26
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
27
|
+
if (Error.captureStackTrace) {
|
|
28
|
+
Error.captureStackTrace(this, AppError);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert error to JSON representation
|
|
34
|
+
*/
|
|
35
|
+
toJSON(): Record<string, any> {
|
|
36
|
+
return {
|
|
37
|
+
name: this.name,
|
|
38
|
+
message: this.message,
|
|
39
|
+
code: this.code,
|
|
40
|
+
statusCode: this.statusCode,
|
|
41
|
+
details: this.details,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Authentication error class
|
|
48
|
+
*/
|
|
49
|
+
export class AuthError extends AppError {
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
code = "AUTH_ERROR",
|
|
53
|
+
statusCode = 401,
|
|
54
|
+
details?: Record<string, any>,
|
|
55
|
+
) {
|
|
56
|
+
super(message, code, statusCode, details);
|
|
57
|
+
this.name = "AuthError";
|
|
58
|
+
|
|
59
|
+
if (Error.captureStackTrace) {
|
|
60
|
+
Error.captureStackTrace(this, AuthError);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validation error class
|
|
67
|
+
*/
|
|
68
|
+
export class ValidationError extends AppError {
|
|
69
|
+
public readonly errors: Array<{ field: string; message: string }>;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
message: string,
|
|
73
|
+
errors: Array<{ field: string; message: string }> = [],
|
|
74
|
+
code = "VALIDATION_ERROR",
|
|
75
|
+
statusCode = 400,
|
|
76
|
+
) {
|
|
77
|
+
super(message, code, statusCode, { errors });
|
|
78
|
+
this.name = "ValidationError";
|
|
79
|
+
this.errors = errors;
|
|
80
|
+
|
|
81
|
+
if (Error.captureStackTrace) {
|
|
82
|
+
Error.captureStackTrace(this, ValidationError);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert error to JSON representation
|
|
88
|
+
*/
|
|
89
|
+
toJSON(): Record<string, any> {
|
|
90
|
+
return {
|
|
91
|
+
...super.toJSON(),
|
|
92
|
+
errors: this.errors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Not found error class
|
|
99
|
+
*/
|
|
100
|
+
export class NotFoundError extends AppError {
|
|
101
|
+
public readonly resource: string;
|
|
102
|
+
public readonly resourceId?: string;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
resource: string,
|
|
106
|
+
resourceId?: string,
|
|
107
|
+
message?: string,
|
|
108
|
+
code = "NOT_FOUND",
|
|
109
|
+
statusCode = 404,
|
|
110
|
+
) {
|
|
111
|
+
const defaultMessage = resourceId
|
|
112
|
+
? `${resource} with id '${resourceId}' not found`
|
|
113
|
+
: `${resource} not found`;
|
|
114
|
+
super(message || defaultMessage, code, statusCode, { resource, resourceId });
|
|
115
|
+
this.name = "NotFoundError";
|
|
116
|
+
this.resource = resource;
|
|
117
|
+
this.resourceId = resourceId;
|
|
118
|
+
|
|
119
|
+
if (Error.captureStackTrace) {
|
|
120
|
+
Error.captureStackTrace(this, NotFoundError);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convert error to JSON representation
|
|
126
|
+
*/
|
|
127
|
+
toJSON(): Record<string, any> {
|
|
128
|
+
return {
|
|
129
|
+
...super.toJSON(),
|
|
130
|
+
resource: this.resource,
|
|
131
|
+
resourceId: this.resourceId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @htlkg/core
|
|
3
|
+
* Core utilities, types, and authentication for Hotelinking applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Auth exports
|
|
7
|
+
export {
|
|
8
|
+
getUser,
|
|
9
|
+
getClientUser,
|
|
10
|
+
hasAccessToBrand,
|
|
11
|
+
hasAccessToAccount,
|
|
12
|
+
isAdminUser,
|
|
13
|
+
isSuperAdminUser,
|
|
14
|
+
requireAuth,
|
|
15
|
+
requireAdminAccess,
|
|
16
|
+
requireBrandAccess,
|
|
17
|
+
type User,
|
|
18
|
+
type APIContext,
|
|
19
|
+
} from "./auth/index.js";
|
|
20
|
+
|
|
21
|
+
// Type exports
|
|
22
|
+
export type {
|
|
23
|
+
Brand,
|
|
24
|
+
Account,
|
|
25
|
+
Product,
|
|
26
|
+
ProductInstance,
|
|
27
|
+
BrandUser,
|
|
28
|
+
} from "./types/index.js";
|
|
29
|
+
|
|
30
|
+
// Utility exports
|
|
31
|
+
export {
|
|
32
|
+
formatDate,
|
|
33
|
+
truncateText,
|
|
34
|
+
groupBy,
|
|
35
|
+
debounce,
|
|
36
|
+
throttle,
|
|
37
|
+
} from "./utils/index.js";
|
|
38
|
+
|
|
39
|
+
// Constants exports
|
|
40
|
+
export { ROUTES, PRODUCTS, PERMISSIONS, USER_ROLES } from "./constants/index.js";
|
|
41
|
+
|
|
42
|
+
// Error exports
|
|
43
|
+
export {
|
|
44
|
+
AppError,
|
|
45
|
+
AuthError,
|
|
46
|
+
ValidationError,
|
|
47
|
+
NotFoundError,
|
|
48
|
+
} from "./errors/index.js";
|
|
49
|
+
|
|
50
|
+
// Type-safe routes
|
|
51
|
+
export {
|
|
52
|
+
routes,
|
|
53
|
+
adminRoutes,
|
|
54
|
+
portalRoutes,
|
|
55
|
+
authRoutes,
|
|
56
|
+
apiRoutes,
|
|
57
|
+
createRoute,
|
|
58
|
+
matchesRoute,
|
|
59
|
+
extractRouteParams,
|
|
60
|
+
navigateTo,
|
|
61
|
+
buildUrl,
|
|
62
|
+
type Route,
|
|
63
|
+
type RouteParams,
|
|
64
|
+
type Routes,
|
|
65
|
+
} from "./routes/index.js";
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-Safe Routes
|
|
3
|
+
*
|
|
4
|
+
* Centralized route definitions with type safety and parameter interpolation.
|
|
5
|
+
* Eliminates string literals scattered across the codebase.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Route parameters type
|
|
10
|
+
*/
|
|
11
|
+
export type RouteParams = Record<string, string | number>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Route function interface - can be called with params or used as path
|
|
15
|
+
*/
|
|
16
|
+
export interface Route<TParams extends RouteParams = RouteParams> {
|
|
17
|
+
/** Base path of the route */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
/** Generate URL with interpolated parameters */
|
|
20
|
+
(params?: TParams): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a route with optional parameters
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const userRoute = createRoute<{ id: string }>('/users/:id');
|
|
29
|
+
* userRoute({ id: '123' }); // "/users/123"
|
|
30
|
+
* userRoute.path; // "/users/:id"
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createRoute<TParams extends RouteParams = Record<string, never>>(path: string): Route<TParams> {
|
|
34
|
+
const routeFn = (params?: TParams): string => {
|
|
35
|
+
if (!params) return path;
|
|
36
|
+
|
|
37
|
+
let result = path;
|
|
38
|
+
for (const [key, value] of Object.entries(params)) {
|
|
39
|
+
result = result.replace(`:${key}`, String(value));
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Add path as a property
|
|
45
|
+
Object.defineProperty(routeFn, "path", {
|
|
46
|
+
value: path,
|
|
47
|
+
writable: false,
|
|
48
|
+
enumerable: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return routeFn as Route<TParams>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Admin routes
|
|
56
|
+
*/
|
|
57
|
+
export const adminRoutes = {
|
|
58
|
+
/** Dashboard */
|
|
59
|
+
dashboard: createRoute("/admin"),
|
|
60
|
+
|
|
61
|
+
/** Accounts list */
|
|
62
|
+
accounts: createRoute("/admin/accounts"),
|
|
63
|
+
|
|
64
|
+
/** Account detail */
|
|
65
|
+
account: createRoute<{ id: string | number }>("/admin/accounts/:id"),
|
|
66
|
+
|
|
67
|
+
/** Account brands */
|
|
68
|
+
accountBrands: createRoute<{ id: string | number }>("/admin/accounts/:id/brands"),
|
|
69
|
+
|
|
70
|
+
/** Brands list */
|
|
71
|
+
brands: createRoute("/admin/brands"),
|
|
72
|
+
|
|
73
|
+
/** Brand detail */
|
|
74
|
+
brand: createRoute<{ id: string | number }>("/admin/brands/:id"),
|
|
75
|
+
|
|
76
|
+
/** Brand settings */
|
|
77
|
+
brandSettings: createRoute<{ id: string | number }>("/admin/brands/:id/settings"),
|
|
78
|
+
|
|
79
|
+
/** Users list */
|
|
80
|
+
users: createRoute("/admin/users"),
|
|
81
|
+
|
|
82
|
+
/** User detail */
|
|
83
|
+
user: createRoute<{ id: string }>("/admin/users/:id"),
|
|
84
|
+
|
|
85
|
+
/** Current user profile */
|
|
86
|
+
profile: createRoute("/admin/user"),
|
|
87
|
+
|
|
88
|
+
/** Analytics */
|
|
89
|
+
analytics: createRoute("/admin/analytics"),
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Portal routes (customer-facing)
|
|
94
|
+
*/
|
|
95
|
+
export const portalRoutes = {
|
|
96
|
+
/** Portal home */
|
|
97
|
+
home: createRoute("/"),
|
|
98
|
+
|
|
99
|
+
/** Brand portal */
|
|
100
|
+
brand: createRoute<{ brandId: string | number }>("/brands/:brandId"),
|
|
101
|
+
|
|
102
|
+
/** Brand settings */
|
|
103
|
+
brandSettings: createRoute<{ brandId: string | number }>("/brands/:brandId/settings"),
|
|
104
|
+
|
|
105
|
+
/** Brand dashboard */
|
|
106
|
+
brandDashboard: createRoute<{ brandId: string | number }>("/brands/:brandId/dashboard"),
|
|
107
|
+
|
|
108
|
+
/** Brand analytics */
|
|
109
|
+
brandAnalytics: createRoute<{ brandId: string | number }>("/brands/:brandId/analytics"),
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Auth routes
|
|
114
|
+
*/
|
|
115
|
+
export const authRoutes = {
|
|
116
|
+
/** Login page */
|
|
117
|
+
login: createRoute("/login"),
|
|
118
|
+
|
|
119
|
+
/** Logout endpoint */
|
|
120
|
+
logout: createRoute("/api/auth/logout"),
|
|
121
|
+
|
|
122
|
+
/** Confirm sign in */
|
|
123
|
+
confirmSignIn: createRoute("/api/auth/confirm-signin"),
|
|
124
|
+
} as const;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* API routes
|
|
128
|
+
*/
|
|
129
|
+
export const apiRoutes = {
|
|
130
|
+
/** Auth endpoints */
|
|
131
|
+
auth: {
|
|
132
|
+
logout: createRoute("/api/auth/logout"),
|
|
133
|
+
confirmSignIn: createRoute("/api/auth/confirm-signin"),
|
|
134
|
+
},
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* All routes combined
|
|
139
|
+
*/
|
|
140
|
+
export const routes = {
|
|
141
|
+
admin: adminRoutes,
|
|
142
|
+
portal: portalRoutes,
|
|
143
|
+
auth: authRoutes,
|
|
144
|
+
api: apiRoutes,
|
|
145
|
+
} as const;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Type for all routes
|
|
149
|
+
*/
|
|
150
|
+
export type Routes = typeof routes;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Helper to check if a path matches a route pattern
|
|
154
|
+
*/
|
|
155
|
+
export function matchesRoute(path: string, route: Route): boolean {
|
|
156
|
+
const pattern = route.path
|
|
157
|
+
.replace(/:[^/]+/g, "[^/]+") // Replace :param with regex
|
|
158
|
+
.replace(/\//g, "\\/"); // Escape slashes
|
|
159
|
+
|
|
160
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
161
|
+
return regex.test(path);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract parameters from a path given a route pattern
|
|
166
|
+
*/
|
|
167
|
+
export function extractRouteParams<TParams extends RouteParams>(
|
|
168
|
+
path: string,
|
|
169
|
+
route: Route<TParams>
|
|
170
|
+
): TParams | null {
|
|
171
|
+
const paramNames: string[] = [];
|
|
172
|
+
const pattern = route.path.replace(/:([^/]+)/g, (_, name) => {
|
|
173
|
+
paramNames.push(name);
|
|
174
|
+
return "([^/]+)";
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
178
|
+
const match = path.match(regex);
|
|
179
|
+
|
|
180
|
+
if (!match) return null;
|
|
181
|
+
|
|
182
|
+
const params: RouteParams = {};
|
|
183
|
+
paramNames.forEach((name, index) => {
|
|
184
|
+
params[name] = match[index + 1];
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return params as TParams;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Navigate to a route (client-side helper)
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* navigateTo(routes.admin.account({ id: '123' }));
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function navigateTo(url: string): void {
|
|
199
|
+
if (typeof window !== "undefined") {
|
|
200
|
+
window.location.href = url;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build a URL with query parameters
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const url = buildUrl(routes.admin.users(), { page: 2, status: 'active' });
|
|
210
|
+
* // "/admin/users?page=2&status=active"
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function buildUrl(path: string, params?: Record<string, string | number | boolean | null | undefined>): string {
|
|
214
|
+
if (!params) return path;
|
|
215
|
+
|
|
216
|
+
const searchParams = new URLSearchParams();
|
|
217
|
+
for (const [key, value] of Object.entries(params)) {
|
|
218
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
219
|
+
searchParams.set(key, String(value));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const queryString = searchParams.toString();
|
|
224
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
225
|
+
}
|