Update TypeScript configuration for stricter type checks and modify environment variable access. Exclude additional directories from compilation, enhance error handling in resolvers, and improve random element selection in mock data generation. Refactor locale handling in middleware and update GraphQL client to use bracket notation for environment variables.

This commit is contained in:
Peter Meier
2025-12-14 00:05:47 +01:00
parent 671a769e43
commit 2966725685
26 changed files with 115 additions and 76 deletions

View File

@@ -36,7 +36,12 @@ const descriptions = [
];
function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
const index = Math.floor(Math.random() * array.length);
const element = array[index];
if (element === undefined) {
throw new Error("Array is empty");
}
return element;
}
function getRandomPrice(): number {
@@ -57,9 +62,14 @@ function generateProduct(id: string): Product {
const promotionType = Math.random();
if (promotionType < 0.33) {
// Sale mit Rabatt
const discountPercent = [10, 20, 30, 40, 50][
Math.floor(Math.random() * 5)
const discountPercentArray = [10, 20, 30, 40, 50];
const discountPercent =
discountPercentArray[
Math.floor(Math.random() * discountPercentArray.length)
];
if (discountPercent === undefined) {
throw new Error("Discount percent array is empty");
}
originalPrice = basePrice;
price = Math.round(basePrice * (1 - discountPercent / 100) * 100) / 100;
promotion = {

View File

@@ -6,7 +6,7 @@ import type { DataAdapter } from "./interface";
* Bestimmt welcher Adapter basierend auf Environment-Variablen verwendet wird
*/
export function createAdapter(): DataAdapter {
const adapterType = process.env.DATA_ADAPTER || "mock";
const adapterType = process.env["DATA_ADAPTER"] || "mock";
switch (adapterType) {
case "mock":

View File

@@ -1,10 +1,11 @@
import jwt from "jsonwebtoken";
import type { JWTPayload, UserRole } from "../types/user.js";
import jwt, { type SignOptions } from "jsonwebtoken";
import type { JWTPayload } from "../types/user.js";
import { UserRole } from "../types/user.js";
import { logger } from "../monitoring/logger.js";
const JWT_SECRET =
process.env.JWT_SECRET || "your-secret-key-change-in-production";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
const JWT_SECRET: string =
process.env["JWT_SECRET"] || "your-secret-key-change-in-production";
const JWT_EXPIRES_IN: string = process.env["JWT_EXPIRES_IN"] || "7d";
/**
* Erstellt ein JWT Token für einen User
@@ -12,7 +13,7 @@ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
export function createToken(payload: JWTPayload): string {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
} as SignOptions);
}
/**
@@ -42,7 +43,7 @@ export function extractTokenFromHeader(
return null;
}
return parts[1];
return parts[1] ?? null;
}
/**

View File

@@ -1,9 +1,5 @@
import type {
User,
UserRole,
LoginCredentials,
RegisterData,
} from "../types/user.js";
import type { User, LoginCredentials, RegisterData } from "../types/user.js";
import { UserRole } from "../types/user.js";
import { hashPassword, comparePassword } from "./password.js";
import { createToken, verifyToken } from "./jwt.js";
import { logger } from "../monitoring/logger.js";
@@ -22,7 +18,7 @@ export class UserService {
*/
async register(
data: RegisterData,
role: UserRole = "customer"
role: UserRole = UserRole.CUSTOMER
): Promise<{
user: User;
token: string;

View File

@@ -4,16 +4,15 @@
*/
export const cacheConfig = {
pages: {
ttl: parseInt(process.env.CACHE_PAGES_TTL || '60000'), // 60 Sekunden
ttl: parseInt(process.env["CACHE_PAGES_TTL"] || "60000"), // 60 Sekunden
},
pageSeo: {
ttl: parseInt(process.env.CACHE_PAGE_SEO_TTL || '300000'), // 5 Minuten
ttl: parseInt(process.env["CACHE_PAGE_SEO_TTL"] || "300000"), // 5 Minuten
},
navigation: {
ttl: parseInt(process.env.CACHE_NAVIGATION_TTL || '300000'), // 5 Minuten
ttl: parseInt(process.env["CACHE_NAVIGATION_TTL"] || "300000"), // 5 Minuten
},
products: {
ttl: parseInt(process.env.CACHE_PRODUCTS_TTL || '30000'), // 30 Sekunden
ttl: parseInt(process.env["CACHE_PRODUCTS_TTL"] || "30000"), // 30 Sekunden
},
} as const;

View File

@@ -2,7 +2,6 @@ import type { DataAdapter } from "./adapters/interface.js";
import { createAdapter } from "./adapters/config.js";
import type { PageSeo, Page, Navigation, Product } from "./types/index.js";
import type { TranslationsData } from "./adapters/Mock/_i18n/mockTranslations.js";
import { AdapterError } from "./utils/errors.js";
import { cache } from "./utils/cache.js";
import { CacheKeyBuilder } from "./utils/cacheKeys.js";
import { DataServiceHelpers } from "./utils/dataServiceHelpers.js";

View File

@@ -15,14 +15,14 @@ import { getMetrics } from "./monitoring/metrics.js";
import { extractTokenFromHeader } from "./auth/jwt.js";
import { userService } from "./auth/userService.js";
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000;
const METRICS_PORT = process.env.METRICS_PORT
? parseInt(process.env.METRICS_PORT)
const PORT = process.env["PORT"] ? parseInt(process.env["PORT"]) : 4000;
const METRICS_PORT = process.env["METRICS_PORT"]
? parseInt(process.env["METRICS_PORT"])
: 9090;
// Konfiguration aus Environment Variables
const MAX_QUERY_COMPLEXITY = process.env.MAX_QUERY_COMPLEXITY
? parseInt(process.env.MAX_QUERY_COMPLEXITY)
const MAX_QUERY_COMPLEXITY = process.env["MAX_QUERY_COMPLEXITY"]
? parseInt(process.env["MAX_QUERY_COMPLEXITY"])
: 1000;
/**

View File

@@ -5,7 +5,7 @@ import winston from "winston";
* Erstellt JSON-Logs für bessere Analyse und Monitoring
*/
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
level: process.env["LOG_LEVEL"] || "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
@@ -13,7 +13,7 @@ export const logger = winston.createLogger({
),
defaultMeta: {
service: "graphql-middlelayer",
environment: process.env.NODE_ENV || "development",
environment: process.env["NODE_ENV"] || "development",
},
transports: [
// Console Output (für Development)
@@ -29,7 +29,7 @@ export const logger = winston.createLogger({
),
}),
// File Output (für Production)
...(process.env.NODE_ENV === "production"
...(process.env["NODE_ENV"] === "production"
? [
new winston.transports.File({
filename: "logs/error.log",

View File

@@ -50,7 +50,7 @@ export const resolvers = {
products: async (
_: unknown,
args: { limit?: number },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(() => dataService.getProducts(args.limit));
},
@@ -71,7 +71,7 @@ export const resolvers = {
pageSeo: async (
_: unknown,
args: { locale?: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(() => dataService.getPageSeo(args.locale));
},
@@ -96,14 +96,14 @@ export const resolvers = {
pages: async (
_: unknown,
args: { locale?: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(() => dataService.getPages(args.locale));
},
homepage: async (
_: unknown,
args: { locale?: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(async () => {
// Homepage ist immer die Seite mit dem Slug "/"
@@ -117,7 +117,7 @@ export const resolvers = {
navigation: async (
_: unknown,
args: { locale?: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(() => dataService.getNavigation(args.locale));
},
@@ -133,7 +133,7 @@ export const resolvers = {
translations: async (
_: unknown,
args: { locale?: string; namespace?: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(() =>
dataService.getTranslations(args.locale, args.namespace)
@@ -144,7 +144,7 @@ export const resolvers = {
register: async (
_: unknown,
args: { email: string; password: string; name: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(async () => {
const result = await userService.register({
@@ -167,7 +167,7 @@ export const resolvers = {
login: async (
_: unknown,
args: { email: string; password: string },
context: GraphQLContext
_context: GraphQLContext
) => {
return withErrorHandling(async () => {
const result = await userService.login({

View File

@@ -9,6 +9,16 @@ import type { c_youtubeVideo } from "./c_youtubeVideo";
import type { c_headline } from "./c_headline";
export type { contentLayout };
export type {
c_html,
c_markdown,
c_iframe,
c_imageGallery,
c_image,
c_quote,
c_youtubeVideo,
c_headline,
};
export type ContentItem =
| c_html
| c_markdown

View File

@@ -64,7 +64,7 @@ class InMemoryCache<T> implements CacheInterface<T> {
* Cache-Instanzen
* Verwendet Redis wenn aktiviert, sonst In-Memory
*/
const useRedis = process.env.REDIS_ENABLED === "true";
const useRedis = process.env["REDIS_ENABLED"] === "true";
export const cache = {
pages: useRedis

View File

@@ -33,7 +33,7 @@ export const createPageLoader = () => {
keys.map((key) => {
// Parse Key: Format kann "slug:locale" oder "slug" sein
const parts = key.split(":");
const slug = parts[0];
const slug = parts[0] ?? "";
const locale = parts.length > 1 ? parts[1] : undefined;
return dataService.getPage(slug, locale);
})

View File

@@ -16,7 +16,7 @@ export class RedisCache<T> {
constructor(defaultTTL: number, cacheType: string) {
this.defaultTTL = defaultTTL;
this.cacheType = cacheType;
this.useRedis = process.env.REDIS_ENABLED === "true";
this.useRedis = process.env["REDIS_ENABLED"] === "true";
if (this.useRedis) {
this.initRedis();
@@ -30,9 +30,9 @@ export class RedisCache<T> {
private async initRedis() {
try {
this.redis = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379"),
password: process.env.REDIS_PASSWORD,
host: process.env["REDIS_HOST"] || "localhost",
port: parseInt(process.env["REDIS_PORT"] || "6379"),
password: process.env["REDIS_PASSWORD"],
retryStrategy: (times) => {
// Bei Fehler: Fallback auf In-Memory nach 3 Versuchen
if (times > 3) {

View File

@@ -86,8 +86,9 @@ export default function LoginModal() {
const result: GraphQLResponse<LoginResponse> = await response.json();
if (result.errors) {
setError(result.errors[0].message || t("login.error"));
if (result.errors && result.errors.length > 0) {
const firstError = result.errors[0];
setError(firstError?.message || t("login.error"));
return;
}
@@ -126,7 +127,9 @@ export default function LoginModal() {
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">{t("login.title")}</h2>
<h2 className="text-2xl font-bold text-gray-900">
{t("login.title")}
</h2>
<button
onClick={close}
className="text-gray-400 hover:text-gray-600 transition-colors"
@@ -217,4 +220,3 @@ export default function LoginModal() {
</div>
);
}

View File

@@ -105,8 +105,9 @@ export default function RegisterModal() {
const result: GraphQLResponse<RegisterResponse> = await response.json();
if (result.errors) {
setError(result.errors[0].message || t("register.error"));
if (result.errors && result.errors.length > 0) {
const firstError = result.errors[0];
setError(firstError?.message || t("register.error"));
return;
}

View File

@@ -114,8 +114,9 @@ export function loginModalData(): any {
const result: GraphQLResponse<LoginResponse> = await response.json();
if (result.errors) {
this.error = result.errors[0].message;
if (result.errors && result.errors.length > 0) {
const firstError = result.errors[0];
this.error = firstError?.message || "An error occurred";
return;
}

View File

@@ -133,8 +133,9 @@ export function registerModalData(): any {
const result: GraphQLResponse<RegisterResponse> = await response.json();
if (result.errors) {
this.error = result.errors[0].message;
if (result.errors && result.errors.length > 0) {
const firstError = result.errors[0];
this.error = firstError?.message || "An error occurred";
return;
}

View File

@@ -1,7 +1,7 @@
import { getToken } from "../auth/authService.js";
const GRAPHQL_URL =
import.meta.env.PUBLIC_GRAPHQL_URL || "http://localhost:4000";
import.meta.env["PUBLIC_GRAPHQL_URL"] || "http://localhost:4000";
interface GraphQLResponse<T> {
data?: T;

View File

@@ -38,9 +38,11 @@ class I18n {
private loadDefaults(): void {
const defaults =
defaultTranslations[this.locale] || defaultTranslations["de"];
if (defaults) {
Object.entries(defaults).forEach(([key, value]) => {
this.translations.set(key, value);
});
}
// Markiere als geladen, damit Defaults sofort verfügbar sind
this.loaded = true;
}
@@ -83,7 +85,7 @@ class I18n {
// Füge namespace nur hinzu, wenn es definiert ist
if (namespace) {
variables.namespace = namespace;
variables["namespace"] = namespace;
}
const query = namespace
@@ -150,7 +152,7 @@ class I18n {
if (!translation) {
const defaults =
defaultTranslations[this.locale] || defaultTranslations["de"];
translation = defaults[key] || key;
translation = defaults?.[key] || key;
}
// Ersetze Platzhalter {param}

View File

@@ -22,11 +22,13 @@ export const onRequest = defineMiddleware((context, next) => {
context.locals.pathWithoutLocale = pathWithoutLocale;
// Setze Locale in Cookie für persistente Auswahl
if (locale) {
context.cookies.set("locale", locale, {
path: "/",
maxAge: 60 * 60 * 24 * 365, // 1 Jahr
sameSite: "lax",
});
}
} else {
// Keine Locale in URL: Browser-Locale oder Cookie verwenden
const cookieLocale = context.cookies.get("locale")?.value;

View File

@@ -6,7 +6,7 @@ import { i18n } from "../../lib/i18n/i18n.js";
import PageContent from "../../components/PageContent.astro";
import type { Page } from "../../lib/types/page.js";
const locale = Astro.params.locale || "de";
const locale = Astro.params["locale"] || "de";
i18n.setLocale(locale);
let page: Page | null = await loadPage("/404", locale, "404-Seite");

View File

@@ -6,7 +6,7 @@ import { i18n } from "../../lib/i18n/i18n.js";
import PageContent from "../../components/PageContent.astro";
import type { Page } from "../../lib/types/page.js";
const locale = Astro.params.locale || "de";
const locale = Astro.params["locale"] || "de";
i18n.setLocale(locale);
let page: Page | null = await loadPage("/500", locale, "500-Seite");

View File

@@ -5,10 +5,10 @@ import { getPage, getHomepage } from "../../lib/graphql/cmsQueries.js";
import { i18n } from "../../lib/i18n/i18n.js";
import ContentRow from "../../components/ContentRow.astro";
const locale = Astro.params.locale || "de";
const locale = Astro.params["locale"] || "de";
// Bei [...slug] kommt slug als String (z.B. "about" für /de/about)
// Konvertiere zu String mit führendem Slash
const slugParam = Astro.params.slug;
const slugParam = Astro.params["slug"];
const slug = slugParam ? `/${slugParam}` : "/";
// Setze Locale im i18n-System

View File

@@ -6,7 +6,7 @@ import { i18n } from "../../lib/i18n/i18n.js";
import PageContent from "../../components/PageContent.astro";
import type { Page } from "../../lib/types/page.js";
const locale = Astro.params.locale || "de";
const locale = Astro.params["locale"] || "de";
i18n.setLocale(locale);
let page: Page | null = await loadPage("/", locale, "Homepage");

View File

@@ -6,7 +6,7 @@ import { getProducts } from "../../../lib/graphql/queries.js";
import { i18n } from "../../../lib/i18n/i18n.js";
import type { Product as ProductType } from "../../../lib/types/product.js";
const locale = Astro.params.locale || "de";
const locale = Astro.params["locale"] || "de";
i18n.setLocale(locale);
let products: ProductType[] = [];

View File

@@ -1,7 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"exclude": ["dist", "middlelayer/__cms/**/*"],
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
@@ -10,6 +10,21 @@
"paths": {
"@middlelayer-types/*": ["middlelayer/types/*"],
"@middlelayer/*": ["middlelayer/*"]
}
},
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true
}
}