Compare commits

...

19 Commits

Author SHA1 Message Date
Gitea Actions
3c0bebb65c ci: Bump version to 0.0.11 [skip ci] 2025-12-23 07:59:09 +05:00
265cc3ffd4 Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m37s
2025-12-22 18:58:11 -08:00
3d5767b60b roll back changes to src/routes/system.routes.ts hopefully before OOM issues 2025-12-22 18:58:01 -08:00
Gitea Actions
e9cb45efe0 ci: Bump version to 0.0.10 [skip ci] 2025-12-23 07:41:54 +05:00
99a57f3a30 Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m47s
2025-12-22 18:40:59 -08:00
e46f5eb7f6 roll back changes to src/routes/system.routes.test.ts hopefully before OOM issues 2025-12-22 18:40:37 -08:00
Gitea Actions
034887069c ci: Bump version to 0.0.9 [skip ci] 2025-12-23 07:23:30 +05:00
84b5e0e15e Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
2025-12-22 18:22:23 -08:00
dc0f774699 try to stop system.route test crashes fuck sakes 2025-12-22 18:21:39 -08:00
Gitea Actions
1195b7e87f ci: Bump version to 0.0.8 [skip ci] 2025-12-23 04:45:54 +05:00
e9889f1f1e Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m41s
2025-12-22 15:45:11 -08:00
3c7f6429aa try to stop system.route test crashes 2025-12-22 15:43:59 -08:00
Gitea Actions
0db90dfaa6 ci: Bump version to 0.0.7 [skip ci] 2025-12-23 04:33:19 +05:00
b7a1294ae6 fix to versioning
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
2025-12-22 15:32:43 -08:00
Gitea Actions
be652f9790 ci: Bump version to 0.0.6 [skip ci] 2025-12-23 04:17:17 +05:00
1a3e6a9ab5 unit test fixes
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 38s
2025-12-22 15:11:18 -08:00
Gitea Actions
262396ddd0 ci: Bump version to 0.0.5 [skip ci] 2025-12-23 02:29:27 +05:00
c542796048 Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
2025-12-22 13:22:28 -08:00
5b8f309ad8 oom issue 2025-12-22 13:22:21 -08:00
11 changed files with 125 additions and 46 deletions

View File

@@ -51,7 +51,14 @@ jobs:
# Bump the patch version number. This creates a new commit and a new tag.
# The commit message includes [skip ci] to prevent this push from triggering another workflow run.
npm version patch -m "ci: Bump version to %s [skip ci]"
# If the tag already exists (e.g. re-running a failed job), we skip the conflicting version.
if ! npm version patch -m "ci: Bump version to %s [skip ci]"; then
echo "⚠️ Version bump failed (likely tag exists). Attempting to skip to next version..."
# Bump package.json to the conflicting version without git tagging
npm version patch --no-git-tag-version > /dev/null
# Bump again to the next version, forcing it because the directory is now dirty
npm version patch -m "ci: Bump version to %s [skip ci]" --force
fi
# Push the new commit and the new tag back to the main branch.
git push --follow-tags

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.0.4",
"version": "0.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.0.4",
"version": "0.0.11",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.0.4",
"version": "0.0.11",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -192,7 +192,7 @@ describe('Auth Routes (/api/auth)', () => {
// Assert
expect(response.status).toBe(201);
expect(response.body.message).toBe('User registered successfully!');
expect(response.body.user.email).toBe(newUserEmail);
expect(response.body.userprofile.user.email).toBe(newUserEmail);
expect(response.body.token).toBeTypeOf('string'); // This was a duplicate, fixed.
expect(db.userRepo.createUser).toHaveBeenCalled();
});
@@ -295,7 +295,7 @@ describe('Auth Routes (/api/auth)', () => {
// Assert
expect(response.status).toBe(200);
// The API now returns a nested UserProfile object
expect(response.body.user).toEqual(
expect(response.body.userprofile).toEqual(
expect.objectContaining({
user_id: 'user-123',
user: expect.objectContaining({

View File

@@ -1,10 +1,10 @@
// src/routes/system.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import { exec, type ExecException, type ExecOptions } from 'child_process'; // Keep this for mocking
import systemRouter from './system.routes'; // This was a duplicate, fixed.
import { exec, type ExecException, type ExecOptions } from 'child_process';
import { geocodingService } from '../services/geocodingService.server';
import { createTestApp } from '../tests/utils/createTestApp';
import { mockLogger } from '../tests/utils/mockLogger';
// FIX: Use the simple factory pattern for child_process to avoid default export issues
vi.mock('child_process', () => {
@@ -30,12 +30,15 @@ vi.mock('../services/geocodingService.server', () => ({
// 3. Mock Logger
vi.mock('../services/logger.server', () => ({
logger: mockLogger,
logger: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
child: vi.fn().mockReturnThis(),
},
}));
// Import the router AFTER all mocks are defined.
import systemRouter from './system.routes';
describe('System Routes (/api/system)', () => {
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });

View File

@@ -1,4 +1,4 @@
// src/routes/system.ts
// src/routes/system.routes.ts
import { Router, Request, Response, NextFunction } from 'express';
import { exec } from 'child_process';
import { z } from 'zod';

View File

@@ -128,7 +128,7 @@ export class AIService {
// This preserves the dependency injection pattern used throughout the class.
this.aiClient = genAI
? {
generateContent: (request) => {
generateContent: async (request) => {
// The model name is now injected here, into every call, as the new SDK requires.
// Architectural guard clause: All requests from this service must have content.
// This prevents sending invalid requests to the API and satisfies TypeScript's strictness.

View File

@@ -52,7 +52,10 @@ export class UserRepository {
);
return res.rows[0];
} catch (error) {
logger.error({ err: error, email }, 'Database error in findUserByEmail');
logger.error(
{ err: error instanceof Error ? error.message : error, email },
'Database error in findUserByEmail',
);
throw new Error('Failed to retrieve user from database.');
}
}
@@ -127,7 +130,10 @@ export class UserRepository {
throw new UniqueConstraintError('A user with this email address already exists.');
}
// The withTransaction helper logs the rollback, so we just log the context here.
logger.error({ err: error, email }, 'Error during createUser transaction');
logger.error(
{ err: error instanceof Error ? error.message : error, email },
'Error during createUser transaction',
);
throw new Error('Failed to create user in database.');
});
}
@@ -182,7 +188,10 @@ export class UserRepository {
return authableProfile;
} catch (error) {
logger.error({ err: error, email }, 'Database error in findUserWithProfileByEmail');
logger.error(
{ err: error instanceof Error ? error.message : error, email },
'Database error in findUserWithProfileByEmail',
);
throw new Error('Failed to retrieve user with profile from database.');
}
}
@@ -205,7 +214,10 @@ export class UserRepository {
return res.rows[0];
} catch (error) {
if (error instanceof NotFoundError) throw error;
logger.error({ err: error, userId }, 'Database error in findUserById');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in findUserById',
);
throw new Error('Failed to retrieve user by ID from database.');
}
}
@@ -229,7 +241,10 @@ export class UserRepository {
return res.rows[0];
} catch (error) {
if (error instanceof NotFoundError) throw error;
logger.error({ err: error, userId }, 'Database error in findUserWithPasswordHashById');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in findUserWithPasswordHashById',
);
throw new Error('Failed to retrieve user with sensitive data by ID from database.');
}
}
@@ -275,7 +290,10 @@ export class UserRepository {
if (error instanceof NotFoundError) {
throw error;
}
logger.error({ err: error, userId }, 'Database error in findUserProfileById');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in findUserProfileById',
);
throw new Error('Failed to retrieve user profile from database.');
}
}
@@ -321,7 +339,10 @@ export class UserRepository {
if (error instanceof NotFoundError) {
throw error;
}
logger.error({ err: error, userId, profileData }, 'Database error in updateUserProfile');
logger.error(
{ err: error instanceof Error ? error.message : error, userId, profileData },
'Database error in updateUserProfile',
);
throw new Error('Failed to update user profile in database.');
}
}
@@ -350,7 +371,10 @@ export class UserRepository {
if (error instanceof NotFoundError) {
throw error;
}
logger.error({ err: error, userId, preferences }, 'Database error in updateUserPreferences');
logger.error(
{ err: error instanceof Error ? error.message : error, userId, preferences },
'Database error in updateUserPreferences',
);
throw new Error('Failed to update user preferences in database.');
}
}
@@ -368,7 +392,10 @@ export class UserRepository {
[passwordHash, userId]
);
} catch (error) {
logger.error({ err: error, userId }, 'Database error in updateUserPassword');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in updateUserPassword',
);
throw new Error('Failed to update user password in database.');
}
}
@@ -382,7 +409,10 @@ export class UserRepository {
try {
await this.db.query('DELETE FROM public.users WHERE user_id = $1', [userId]);
} catch (error) {
logger.error({ err: error, userId }, 'Database error in deleteUserById');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in deleteUserById',
);
throw new Error('Failed to delete user from database.');
}
}
@@ -400,7 +430,10 @@ export class UserRepository {
[refreshToken, userId]
);
} catch (error) {
logger.error({ err: error, userId }, 'Database error in saveRefreshToken');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in saveRefreshToken',
);
throw new Error('Failed to save refresh token.');
}
}
@@ -423,7 +456,10 @@ export class UserRepository {
return res.rows[0];
} catch (error) {
if (error instanceof NotFoundError) throw error;
logger.error({ err: error }, 'Database error in findUserByRefreshToken');
logger.error(
{ err: error instanceof Error ? error.message : error },
'Database error in findUserByRefreshToken',
);
throw new Error('Failed to find user by refresh token.'); // Generic error for other failures
}
}
@@ -438,7 +474,10 @@ export class UserRepository {
refreshToken,
]);
} catch (error) {
logger.error({ err: error }, 'Database error in deleteRefreshToken');
logger.error(
{ err: error instanceof Error ? error.message : error },
'Database error in deleteRefreshToken',
);
}
}
@@ -461,7 +500,10 @@ export class UserRepository {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('The specified user does not exist.');
}
logger.error({ err: error, userId }, 'Database error in createPasswordResetToken');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in createPasswordResetToken',
);
throw new Error('Failed to create password reset token.');
}
}
@@ -478,7 +520,10 @@ export class UserRepository {
);
return res.rows;
} catch (error) {
logger.error({ err: error }, 'Database error in getValidResetTokens');
logger.error(
{ err: error instanceof Error ? error.message : error },
'Database error in getValidResetTokens',
);
throw new Error('Failed to retrieve valid reset tokens.');
}
}
@@ -492,7 +537,10 @@ export class UserRepository {
try {
await this.db.query('DELETE FROM public.password_reset_tokens WHERE token_hash = $1', [tokenHash]);
} catch (error) {
logger.error({ err: error, tokenHash }, 'Database error in deleteResetToken');
logger.error(
{ err: error instanceof Error ? error.message : error, tokenHash },
'Database error in deleteResetToken',
);
}
}
@@ -511,7 +559,10 @@ export class UserRepository {
);
return res.rowCount ?? 0;
} catch (error) {
logger.error({ err: error }, 'Database error in deleteExpiredResetTokens');
logger.error(
{ err: error instanceof Error ? error.message : error },
'Database error in deleteExpiredResetTokens',
);
throw new Error('Failed to delete expired password reset tokens.');
}
}
@@ -530,7 +581,10 @@ export class UserRepository {
if (error instanceof Error && 'code' in error && error.code === '23503') {
throw new ForeignKeyConstraintError('One or both users do not exist.');
}
logger.error({ err: error, followerId, followingId }, 'Database error in followUser');
logger.error(
{ err: error instanceof Error ? error.message : error, followerId, followingId },
'Database error in followUser',
);
throw new Error('Failed to follow user.');
}
}
@@ -547,7 +601,10 @@ export class UserRepository {
[followerId, followingId],
);
} catch (error) {
logger.error({ err: error, followerId, followingId }, 'Database error in unfollowUser');
logger.error(
{ err: error instanceof Error ? error.message : error, followerId, followingId },
'Database error in unfollowUser',
);
throw new Error('Failed to unfollow user.');
}
}
@@ -578,7 +635,10 @@ export class UserRepository {
const res = await this.db.query<ActivityLogItem>(query, [userId, limit, offset]);
return res.rows;
} catch (error) {
logger.error({ err: error, userId, limit, offset }, 'Database error in getUserFeed');
logger.error(
{ err: error instanceof Error ? error.message : error, userId, limit, offset },
'Database error in getUserFeed',
);
throw new Error('Failed to retrieve user feed.');
}
}
@@ -600,7 +660,10 @@ export class UserRepository {
);
return res.rows[0];
} catch (error) {
logger.error({ err: error, queryData }, 'Database error in logSearchQuery');
logger.error(
{ err: error instanceof Error ? error.message : error, queryData },
'Database error in logSearchQuery',
);
throw new Error('Failed to log search query.');
}
}
@@ -634,7 +697,10 @@ export async function exportUserData(userId: string, logger: Logger): Promise<{
return { profile, watchedItems, shoppingLists };
});
} catch (error) {
logger.error({ err: error, userId }, 'Database error in exportUserData');
logger.error(
{ err: error instanceof Error ? error.message : error, userId },
'Database error in exportUserData',
);
throw new Error('Failed to export user data.');
}
}

View File

@@ -87,7 +87,7 @@ describe('Geocoding Service', () => {
// Assert
expect(result).toEqual(coordinates);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error), cacheKey: expect.any(String) },
{ err: 'Redis down', cacheKey: expect.any(String) },
'Redis GET or JSON.parse command failed. Proceeding without cache.',
);
expect(mockGoogleService.geocode).toHaveBeenCalled(); // Should still proceed to fetch
@@ -107,7 +107,7 @@ describe('Geocoding Service', () => {
expect(mocks.mockRedis.get).toHaveBeenCalledWith(cacheKey);
// The service should log the JSON parsing error and continue
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(SyntaxError), cacheKey: expect.any(String) },
{ err: expect.any(String), cacheKey: expect.any(String) },
'Redis GET or JSON.parse command failed. Proceeding without cache.',
);
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
@@ -185,7 +185,7 @@ describe('Geocoding Service', () => {
// Assert
expect(result).toEqual(coordinates);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
{ err: 'Network Error' },
expect.stringContaining('An error occurred while calling the Google Maps Geocoding API'),
);
expect(mockNominatimService.geocode).toHaveBeenCalledWith(address, logger);
@@ -223,7 +223,7 @@ describe('Geocoding Service', () => {
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
expect(mocks.mockRedis.set).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
{ err: expect.any(Error), cacheKey: expect.any(String) },
{ err: 'Redis SET failed', cacheKey: expect.any(String) },
'Redis SET command failed. Result will not be cached.',
);
});
@@ -271,7 +271,7 @@ describe('Geocoding Service', () => {
// Act & Assert
await expect(geocodingService.clearGeocodeCache(logger)).rejects.toThrow(redisError);
expect(logger.error).toHaveBeenCalledWith(
{ err: redisError },
{ err: redisError.message },
'Failed to clear geocode cache from Redis.',
);
expect(mocks.mockRedis.del).not.toHaveBeenCalled();

View File

@@ -26,7 +26,7 @@ export class GeocodingService {
}
} catch (error) {
logger.error(
{ err: error, cacheKey },
{ err: error instanceof Error ? error.message : error, cacheKey },
'Redis GET or JSON.parse command failed. Proceeding without cache.',
);
}
@@ -44,7 +44,7 @@ export class GeocodingService {
);
} catch (error) {
logger.error(
{ err: error },
{ err: error instanceof Error ? error.message : error },
'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.',
);
}
@@ -73,7 +73,7 @@ export class GeocodingService {
await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
} catch (error) {
logger.error(
{ err: error, cacheKey },
{ err: error instanceof Error ? error.message : error, cacheKey },
'Redis SET command failed. Result will not be cached.',
);
}
@@ -98,7 +98,10 @@ export class GeocodingService {
logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
return totalDeleted;
} catch (error) {
logger.error({ err: error }, 'Failed to clear geocode cache from Redis.');
logger.error(
{ err: error instanceof Error ? error.message : error },
'Failed to clear geocode cache from Redis.',
);
throw error;
}
}

View File

@@ -45,7 +45,7 @@ export class GoogleGeocodingService {
return null;
} catch (error) {
logger.error(
{ err: error, address },
{ err: error instanceof Error ? error.message : error, address },
'[GoogleGeocodingService] An error occurred while calling the Google Maps API.',
);
throw error; // Re-throw to allow the calling service to handle the failure (e.g., by falling back).