feat: Integrate Directus CMS, add i18n with next-intl, and configure project tooling with pnpm, husky, and commitlint.**
This commit is contained in:
3
lib/services/analytics/analytics-service.ts
Normal file
3
lib/services/analytics/analytics-service.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface AnalyticsService {
|
||||
trackEvent(name: string, properties?: Record<string, unknown>): void;
|
||||
}
|
||||
5
lib/services/analytics/noop-analytics-service.ts
Normal file
5
lib/services/analytics/noop-analytics-service.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { AnalyticsService } from "./analytics-service";
|
||||
|
||||
export class NoopAnalyticsService implements AnalyticsService {
|
||||
trackEvent() {}
|
||||
}
|
||||
15
lib/services/app-services.ts
Normal file
15
lib/services/app-services.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { AnalyticsService } from "./analytics/analytics-service";
|
||||
import type { CacheService } from "./cache/cache-service";
|
||||
import type { ErrorReportingService } from "./errors/error-reporting-service";
|
||||
import type { LoggerService } from "./logging/logger-service";
|
||||
import type { NotificationService } from "./notifications/notification-service";
|
||||
|
||||
export class AppServices {
|
||||
constructor(
|
||||
public readonly analytics: AnalyticsService,
|
||||
public readonly errors: ErrorReportingService,
|
||||
public readonly cache: CacheService,
|
||||
public readonly logger: LoggerService,
|
||||
public readonly notifications: NotificationService,
|
||||
) {}
|
||||
}
|
||||
5
lib/services/cache/cache-service.ts
vendored
Normal file
5
lib/services/cache/cache-service.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CacheService {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
26
lib/services/cache/memory-cache-service.ts
vendored
Normal file
26
lib/services/cache/memory-cache-service.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { CacheService } from "./cache-service";
|
||||
|
||||
export class MemoryCacheService implements CacheService {
|
||||
private cache = new Map<string, { value: any; expiry: number | null }>();
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const item = this.cache.get(key);
|
||||
if (!item) return null;
|
||||
|
||||
if (item.expiry && item.expiry < Date.now()) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.value as T;
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
||||
const expiry = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
|
||||
this.cache.set(key, { value, expiry });
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
44
lib/services/create-services.server.ts
Normal file
44
lib/services/create-services.server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AppServices } from "./app-services";
|
||||
import { NoopAnalyticsService } from "./analytics/noop-analytics-service";
|
||||
import { MemoryCacheService } from "./cache/memory-cache-service";
|
||||
import { GlitchtipErrorReportingService } from "./errors/glitchtip-error-reporting-service";
|
||||
import { NoopErrorReportingService } from "./errors/noop-error-reporting-service";
|
||||
import { GotifyNotificationService } from "./notifications/gotify-notification-service";
|
||||
import { NoopNotificationService } from "./notifications/noop-notification-service";
|
||||
import { PinoLoggerService } from "./logging/pino-logger-service";
|
||||
import { config, getMaskedConfig } from "../config";
|
||||
|
||||
let singleton: AppServices | undefined;
|
||||
|
||||
export function getServerAppServices(): AppServices {
|
||||
if (singleton) return singleton;
|
||||
|
||||
const logger = new PinoLoggerService("server");
|
||||
|
||||
logger.info("Initializing server application services", {
|
||||
environment: getMaskedConfig(),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const analytics = new NoopAnalyticsService();
|
||||
|
||||
const notifications = config.notifications.gotify.enabled
|
||||
? new GotifyNotificationService({
|
||||
url: config.notifications.gotify.url!,
|
||||
token: config.notifications.gotify.token!,
|
||||
enabled: true,
|
||||
})
|
||||
: new NoopNotificationService();
|
||||
|
||||
const errors = config.errors.glitchtip.enabled
|
||||
? new GlitchtipErrorReportingService({ enabled: true }, notifications)
|
||||
: new NoopErrorReportingService();
|
||||
|
||||
const cache = new MemoryCacheService();
|
||||
|
||||
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||
|
||||
logger.info("All application services initialized successfully");
|
||||
|
||||
return singleton;
|
||||
}
|
||||
4
lib/services/errors/error-reporting-service.ts
Normal file
4
lib/services/errors/error-reporting-service.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ErrorReportingService {
|
||||
captureException(error: unknown, context?: Record<string, unknown>): void;
|
||||
captureMessage(message: string, context?: Record<string, unknown>): void;
|
||||
}
|
||||
48
lib/services/errors/glitchtip-error-reporting-service.ts
Normal file
48
lib/services/errors/glitchtip-error-reporting-service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import type { ErrorReportingService } from "./error-reporting-service";
|
||||
import type { NotificationService } from "../notifications/notification-service";
|
||||
|
||||
export interface GlitchtipConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
constructor(
|
||||
private readonly config: GlitchtipConfig,
|
||||
private readonly notifications?: NotificationService,
|
||||
) {}
|
||||
|
||||
captureException(error: unknown, context?: Record<string, unknown>) {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
scope.setExtras(context);
|
||||
}
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
|
||||
if (this.notifications) {
|
||||
this.notifications
|
||||
.notify({
|
||||
title: "🚨 Exception Captured",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
priority: 10,
|
||||
})
|
||||
.catch((err) =>
|
||||
console.error("Failed to send notification for exception", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
captureMessage(message: string, context?: Record<string, unknown>) {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
scope.setExtras(context);
|
||||
}
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
6
lib/services/errors/noop-error-reporting-service.ts
Normal file
6
lib/services/errors/noop-error-reporting-service.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ErrorReportingService } from "./error-reporting-service";
|
||||
|
||||
export class NoopErrorReportingService implements ErrorReportingService {
|
||||
captureException() {}
|
||||
captureMessage() {}
|
||||
}
|
||||
7
lib/services/logging/logger-service.ts
Normal file
7
lib/services/logging/logger-service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface LoggerService {
|
||||
debug(message: string, context?: Record<string, unknown>): void;
|
||||
info(message: string, context?: Record<string, unknown>): void;
|
||||
warn(message: string, context?: Record<string, unknown>): void;
|
||||
error(message: string, context?: Record<string, unknown>): void;
|
||||
child(context: Record<string, unknown>): LoggerService;
|
||||
}
|
||||
56
lib/services/logging/pino-logger-service.ts
Normal file
56
lib/services/logging/pino-logger-service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { pino, type Logger as PinoLogger } from "pino";
|
||||
import type { LoggerService } from "./logger-service";
|
||||
import { config } from "../../config";
|
||||
|
||||
export class PinoLoggerService implements LoggerService {
|
||||
private logger: PinoLogger;
|
||||
|
||||
constructor(name?: string, parent?: PinoLogger) {
|
||||
if (parent) {
|
||||
this.logger = parent.child({ name });
|
||||
} else {
|
||||
const useTransport =
|
||||
config.isDevelopment && typeof window === "undefined";
|
||||
|
||||
this.logger = pino({
|
||||
name: name || "app",
|
||||
level: config.logging.level,
|
||||
transport: useTransport
|
||||
? {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.debug(context, message);
|
||||
else this.logger.debug(message);
|
||||
}
|
||||
|
||||
info(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.info(context, message);
|
||||
else this.logger.info(message);
|
||||
}
|
||||
|
||||
warn(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.warn(context, message);
|
||||
else this.logger.warn(message);
|
||||
}
|
||||
|
||||
error(message: string, context?: Record<string, unknown>) {
|
||||
if (context) this.logger.error(context, message);
|
||||
else this.logger.error(message);
|
||||
}
|
||||
|
||||
child(context: Record<string, unknown>): LoggerService {
|
||||
const childPino = this.logger.child(context);
|
||||
const service = new PinoLoggerService();
|
||||
service.logger = childPino;
|
||||
return service;
|
||||
}
|
||||
}
|
||||
44
lib/services/notifications/gotify-notification-service.ts
Normal file
44
lib/services/notifications/gotify-notification-service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type {
|
||||
NotificationMessage,
|
||||
NotificationService,
|
||||
} from "./notification-service";
|
||||
|
||||
export interface GotifyConfig {
|
||||
url: string;
|
||||
token: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class GotifyNotificationService implements NotificationService {
|
||||
constructor(private readonly config: GotifyConfig) {}
|
||||
|
||||
async notify(message: NotificationMessage): Promise<void> {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.config.url}/message?token=${this.config.token}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: message.title,
|
||||
message: message.message,
|
||||
priority: message.priority ?? 5,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"Failed to send Gotify notification",
|
||||
await response.text(),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending Gotify notification", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
lib/services/notifications/noop-notification-service.ts
Normal file
5
lib/services/notifications/noop-notification-service.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { NotificationService } from "./notification-service";
|
||||
|
||||
export class NoopNotificationService implements NotificationService {
|
||||
async notify() {}
|
||||
}
|
||||
9
lib/services/notifications/notification-service.ts
Normal file
9
lib/services/notifications/notification-service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface NotificationMessage {
|
||||
title: string;
|
||||
message: string;
|
||||
priority?: number; // 0-10, Gotify style
|
||||
}
|
||||
|
||||
export interface NotificationService {
|
||||
notify(message: NotificationMessage): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user