From 7d6664e684e94b2a94d7ee85bb1a2063b2d2a30b Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 8 Feb 2026 12:04:09 +0100 Subject: [PATCH] fix: auto-detect data directory in monorepo without DATA_DIR env var (#117) - getDataDir() now detects monorepo by checking for ../docker-compose.yml - DATA_DIR env var removed from .env and .env.example (no longer needed for local dev) - Docker compose files explicitly set DATA_DIR=/app/data for containers - Updated tests for monorepo detection logic --- .env.example | 5 ---- backend/src/db/db-utils.ts | 20 ++++++++++++---- backend/src/test/database.test.ts | 40 +++++++++++++++++++------------ docker-compose.dev.yml | 2 ++ docker-compose.yml | 1 + frontend/package-lock.json | 27 ++++++++------------- 6 files changed, 54 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 48567da..2c06b1f 100644 --- a/.env.example +++ b/.env.example @@ -13,11 +13,6 @@ PORT=3000 CORS_ORIGINS=http://localhost:4174 LOG_LEVEL=info -# Data directory (where database, images, and state files are stored) -# Default: ./data relative to process.cwd() -# For local dev (cd backend && npm run dev), set to ../data so dev and prod share the same folder -# DATA_DIR=../data - # Timezone for scheduled reminders (e.g., Europe/Berlin, America/New_York) TZ=Europe/Berlin diff --git a/backend/src/db/db-utils.ts b/backend/src/db/db-utils.ts index c5a78ca..29bc1fd 100644 --- a/backend/src/db/db-utils.ts +++ b/backend/src/db/db-utils.ts @@ -23,12 +23,24 @@ const migrationsFolder = resolve(__dirname, "../../drizzle"); /** * Get the data directory path. - * Checks DATA_DIR env var first, then falls back to resolve(cwd, "data"). - * This ensures local dev (`cd backend && npm run dev`) and Docker both - * use the same directory when DATA_DIR is set. + * + * Resolution order: + * 1. DATA_DIR env var (set by docker-compose for containers) + * 2. Monorepo detection: if ../docker-compose.yml exists, we're in backend/ + * subdirectory → use ../data (project root's data folder) + * 3. Fallback: resolve(cwd, "data") (running from project root or standalone) */ export function getDataDir(cwd: string = process.cwd()): string { - return process.env.DATA_DIR ? resolve(process.env.DATA_DIR) : resolve(cwd, "data"); + // Docker containers set DATA_DIR explicitly + if (process.env.DATA_DIR) return resolve(process.env.DATA_DIR); + + // Local dev: detect if we're in backend/ subdirectory of the monorepo + if (existsSync(resolve(cwd, "..", "docker-compose.yml"))) { + return resolve(cwd, "..", "data"); + } + + // Default: data/ relative to cwd (running from project root) + return resolve(cwd, "data"); } /** Build the database URL from a path */ diff --git a/backend/src/test/database.test.ts b/backend/src/test/database.test.ts index 9e1a7a9..9aab9c7 100644 --- a/backend/src/test/database.test.ts +++ b/backend/src/test/database.test.ts @@ -156,9 +156,9 @@ describe("Database Client Utilities", () => { } }); - it("should use DATA_DIR env var when set", () => { - process.env.DATA_DIR = "/custom/data"; - expect(getDataDir()).toBe("/custom/data"); + it("should use DATA_DIR env var when set (Docker)", () => { + process.env.DATA_DIR = "/app/data"; + expect(getDataDir()).toBe("/app/data"); }); it("should resolve relative DATA_DIR to absolute", () => { @@ -168,12 +168,22 @@ describe("Database Client Utilities", () => { expect(result).toMatch(/\/data$/); }); - it("should fall back to cwd/data when DATA_DIR is not set", () => { + it("should detect monorepo and use ../data when in backend/ subdir", () => { delete process.env.DATA_DIR; - expect(getDataDir("/app")).toBe("/app/data"); + // Tests run from backend/ which has ../docker-compose.yml + const result = getDataDir(); + // Should resolve to the project root's data/ folder, not backend/data/ + expect(result).toMatch(/\/data$/); + expect(result).not.toMatch(/backend\/data$/); }); - it("should use DATA_DIR even when cwd is provided", () => { + it("should fall back to cwd/data when not in monorepo", () => { + delete process.env.DATA_DIR; + // Use a directory that has no ../docker-compose.yml + expect(getDataDir("/tmp")).toBe("/tmp/data"); + }); + + it("should prefer DATA_DIR over monorepo detection", () => { process.env.DATA_DIR = "/override/data"; expect(getDataDir("/app")).toBe("/override/data"); }); @@ -190,27 +200,27 @@ describe("Database Client Utilities", () => { } }); - it("should return correct paths based on cwd", () => { - delete process.env.DATA_DIR; + it("should return correct paths with DATA_DIR set", () => { + process.env.DATA_DIR = "/app/data"; const paths = getDbPaths("/app"); expect(paths.dataDir).toBe("/app/data"); expect(paths.dbPath).toBe("/app/data/medassist-ng.db"); expect(paths.url).toBe("file:/app/data/medassist-ng.db"); }); + it("should return correct paths without DATA_DIR in non-monorepo dir", () => { + delete process.env.DATA_DIR; + const paths = getDbPaths("/tmp"); + expect(paths.dataDir).toBe("/tmp/data"); + expect(paths.dbPath).toBe("/tmp/data/medassist-ng.db"); + }); + it("should use process.cwd() by default", () => { delete process.env.DATA_DIR; const paths = getDbPaths(); expect(paths.dataDir).toContain("data"); expect(paths.dbPath).toContain("medassist-ng.db"); }); - - it("should respect DATA_DIR env var", () => { - process.env.DATA_DIR = "/custom/data"; - const paths = getDbPaths("/app"); - expect(paths.dataDir).toBe("/custom/data"); - expect(paths.dbPath).toBe("/custom/data/medassist-ng.db"); - }); }); describe("ensureDataDirectory", () => { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4623e19..6ec7b21 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,6 +9,8 @@ services: - ./data:/app/data env_file: - .env + environment: + - DATA_DIR=/app/data ports: - "3000:3000" security_opt: diff --git a/docker-compose.yml b/docker-compose.yml index 23b710c..388b874 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: environment: - PUID=${PUID:-1000} - PGID=${PGID:-1000} + - DATA_DIR=/app/data volumes: - ./data:/app/data ports: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f43fdfd..08dc3af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -133,7 +133,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -655,7 +654,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -699,7 +697,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1647,7 +1644,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1739,7 +1737,6 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1751,7 +1748,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -1958,6 +1954,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -1968,6 +1965,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2054,7 +2052,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2238,7 +2235,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.5.267", @@ -2468,7 +2466,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.10" }, @@ -2558,7 +2555,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -2647,6 +2643,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -2796,7 +2793,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2886,6 +2882,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -2910,7 +2907,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2923,7 +2919,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2963,7 +2958,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -3277,7 +3273,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3323,7 +3318,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3399,7 +3393,6 @@ "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17",