Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8c48e8c75 | |||
| 051edb5f78 | |||
| f87f6dd336 | |||
| 2eb387811d | |||
| db27c9224d | |||
| 7f6e6fdfbc | |||
| 5f5f610060 | |||
| 809841d672 | |||
| 276eb563bf | |||
| 7624ba0bc0 | |||
| 1063231107 | |||
| 2cd5dee612 | |||
| 8ac1cbaf3e | |||
| bd7e6a37cd | |||
| 4e0799f9d9 | |||
| 223a06ba27 | |||
| 503657d168 | |||
| cf68e281e8 |
@@ -18,7 +18,7 @@ jobs:
|
||||
bun-version: "1.3.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Generate API Prisma client
|
||||
run: DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
bun-version: "1.3.6"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Generate Dalpuri Prisma client (CW MSSQL)
|
||||
run: DATABASE_URL="sqlserver://localhost:1433;database=dummy;user=dummy;password=dummy;trustServerCertificate=true" bunx prisma generate
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
bun-version: "1.3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun run test:unit -- --run
|
||||
|
||||
+32
-9
@@ -17,7 +17,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile --production
|
||||
RUN bun install --production
|
||||
|
||||
# ---- Stage 2: Build ----
|
||||
FROM oven/bun:1.3.11 AS build
|
||||
@@ -32,7 +32,7 @@ COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
# Install all deps (including dev) for the full workspace
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
# Copy API source and config
|
||||
COPY api/src/ ./api/src/
|
||||
@@ -90,6 +90,10 @@ COPY --from=build /app/dalpuri/generated/ ./dalpuri/generated/
|
||||
# Copy production node_modules (Prisma adapter needs native bindings)
|
||||
COPY --from=deps /app/node_modules/ ./node_modules/
|
||||
|
||||
# Copy bun so prisma migrate deploy can run at container startup
|
||||
COPY --from=build /usr/local/bin/bun /usr/local/bin/bun
|
||||
RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx
|
||||
|
||||
# Ensure pdfmake Roboto fonts are present at runtime for PDF generation.
|
||||
COPY --from=build /app/api/node_modules/pdfmake/build/fonts/ ./node_modules/pdfmake/build/fonts/
|
||||
|
||||
@@ -104,7 +108,7 @@ COPY --from=build /app/api/logo.png ./logo.png
|
||||
COPY --from=build /app/api/src/modules/sales-utils/salesTaxRates.json ./salesTaxRates.json
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./server"]
|
||||
CMD ["sh", "-c", "bunx prisma migrate deploy && ./server"]
|
||||
|
||||
# ---- Stage 5: Worker runtime image ----
|
||||
FROM runtime-base AS worker
|
||||
@@ -128,15 +132,13 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
COPY api/prisma/ ./api/prisma/
|
||||
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||
|
||||
RUN chmod +x /app/api/prisma/migrate-entrypoint.sh
|
||||
|
||||
WORKDIR /app/api
|
||||
CMD ["sh", "prisma/migrate-entrypoint.sh"]
|
||||
CMD ["bunx", "prisma", "migrate", "deploy"]
|
||||
|
||||
# ---- Stage 7: Dalpuri CW-to-API sync runner ----
|
||||
FROM oven/bun:1.3.11 AS dalpuri-sync
|
||||
@@ -149,7 +151,7 @@ COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun install
|
||||
|
||||
COPY dalpuri/src/ ./dalpuri/src/
|
||||
COPY dalpuri/prisma/ ./dalpuri/prisma/
|
||||
@@ -166,4 +168,25 @@ WORKDIR /app/api
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
WORKDIR /app/dalpuri
|
||||
CMD ["bun", "run", "src/sync.ts"]
|
||||
CMD ["bun", "run", "src/sync.ts"]
|
||||
|
||||
FROM oven/bun:1.3.11 AS setup-admin
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY api/package.json ./api/package.json
|
||||
COPY dalpuri/package.json ./dalpuri/package.json
|
||||
COPY ui/package.json ./ui/package.json
|
||||
COPY patches ./patches
|
||||
|
||||
RUN bun install
|
||||
|
||||
COPY api/prisma/ ./api/prisma/
|
||||
COPY api/prisma.config.ts ./api/prisma.config.ts
|
||||
COPY api/setup-admin.ts ./api/setup-admin.ts
|
||||
|
||||
WORKDIR /app/api
|
||||
RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" bunx prisma generate
|
||||
|
||||
CMD ["bun", "run", "setup-admin.ts"]
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { defineConfig, env } from 'prisma/config'
|
||||
|
||||
export default defineConfig({
|
||||
export default {
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url: env('DATABASE_URL'),
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { PrismaClient } from './generated/prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Create the admin role if it doesn't exist
|
||||
const adminRole = await prisma.role.upsert({
|
||||
where: { moniker: 'admin' },
|
||||
update: {},
|
||||
create: {
|
||||
title: 'Administrator',
|
||||
moniker: 'admin',
|
||||
permissions: JSON.stringify({
|
||||
// Full permissions for admin
|
||||
'*': true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✓ Admin role created/verified:', adminRole);
|
||||
|
||||
// Find the user with jackson.roberts@totaltech.net
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: 'jackson.roberts@totaltech.net' },
|
||||
include: { roles: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.error(
|
||||
'✗ User jackson.roberts@totaltech.net not found. Please ensure the user exists.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✓ User found:', user.email);
|
||||
|
||||
// Check if user already has admin role
|
||||
const hasAdminRole = user.roles.some((r) => r.moniker === 'admin');
|
||||
|
||||
if (hasAdminRole) {
|
||||
console.log('✓ User already has admin role');
|
||||
} else {
|
||||
// Assign admin role to user
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
roles: {
|
||||
connect: { id: adminRole.id },
|
||||
},
|
||||
},
|
||||
include: { roles: true },
|
||||
});
|
||||
|
||||
console.log('✓ Admin role assigned to jackson.roberts@totaltech.net');
|
||||
console.log('✓ User roles:', updatedUser.roles.map((r) => r.moniker));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('✗ Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -2,7 +2,7 @@ import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { getBoss } from "../../workert";
|
||||
import { getBoss } from "../../boss-instance";
|
||||
import { WorkerQueue } from "../../modules/workers/queues";
|
||||
|
||||
/* POST /v1/cw/sync/full */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Shared PgBoss singleton — kept in its own module to break circular imports
|
||||
* between workert.ts and the worker modules that call getBoss().
|
||||
*/
|
||||
import { PgBoss } from "pg-boss";
|
||||
|
||||
function makePgBossUrl(rawUrl: string): string {
|
||||
try {
|
||||
const u = new URL(rawUrl);
|
||||
// 30-second statement timeout to prevent individual SQL queries from
|
||||
// hanging indefinitely if the DB server stops responding mid-query.
|
||||
u.searchParams.set("options", "-c statement_timeout=30000");
|
||||
return u.toString();
|
||||
} catch {
|
||||
return rawUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export const boss = new PgBoss({
|
||||
connectionString: makePgBossUrl(process.env.DATABASE_URL!),
|
||||
connectionTimeoutMillis: 15_000,
|
||||
});
|
||||
|
||||
boss.on("error", (err) => {
|
||||
console.error("[worker] PgBoss error", err);
|
||||
});
|
||||
|
||||
export function getBoss(): PgBoss {
|
||||
return boss;
|
||||
}
|
||||
+2
-1
@@ -6,7 +6,8 @@ import { events } from "./modules/globalEvents";
|
||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||
import { signPermissions } from "./modules/permission-utils/signPermissions";
|
||||
import { RoleController } from "./controllers/RoleController";
|
||||
import { initializeWorkerSystem, getBoss } from "./workert";
|
||||
import { initializeWorkerSystem } from "./workert";
|
||||
import { getBoss } from "./boss-instance";
|
||||
import { WorkerQueue } from "./modules/workers/queues";
|
||||
import { enqueueIncrementalSync } from "./modules/workers/incremental-sync";
|
||||
import { startCommsServer } from "./modules/workers/coms";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBoss } from "../../workert";
|
||||
import { getBoss } from "../../boss-instance";
|
||||
import { WorkerQueue } from "./queues";
|
||||
|
||||
/**
|
||||
|
||||
+32
-22
@@ -1,13 +1,7 @@
|
||||
import { PgBoss } from "pg-boss";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { WorkerQueue } from "./modules/workers/queues";
|
||||
import { setupEventDebugger } from "./modules/logging/eventDebugger";
|
||||
|
||||
const boss = new PgBoss(process.env.DATABASE_URL!);
|
||||
|
||||
boss.on("error", (err) => {
|
||||
console.error("[worker] PgBoss error", err);
|
||||
});
|
||||
import { boss, getBoss } from "./boss-instance";
|
||||
|
||||
let bossStartPromise: Promise<void> | null = null;
|
||||
let reservationQueueReady = false;
|
||||
@@ -111,19 +105,25 @@ export async function reserveWorkerId(queueType: WorkerQueue): Promise<string> {
|
||||
|
||||
async function ensureDalpuriSyncQueue(): Promise<void> {
|
||||
try {
|
||||
console.log("[worker] Creating DALPURI_FULL_SYNC queue...");
|
||||
await boss.createQueue(WorkerQueue.DALPURI_FULL_SYNC);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] DALPURI_FULL_SYNC queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] DALPURI_FULL_SYNC queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log("[worker] Creating DALPURI_INCREMENTAL_SYNC queue...");
|
||||
await boss.createQueue(WorkerQueue.DALPURI_INCREMENTAL_SYNC);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
try {
|
||||
console.log("[worker] Creating REFRESH_SALES_METRICS queue...");
|
||||
await boss.createQueue(WorkerQueue.REFRESH_SALES_METRICS);
|
||||
} catch {
|
||||
// Queue may already exist; ignore to keep this idempotent.
|
||||
console.log("[worker] REFRESH_SALES_METRICS queue ready");
|
||||
} catch (err) {
|
||||
console.log("[worker] REFRESH_SALES_METRICS queue already exists (or error):", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,14 +138,6 @@ export async function initializeWorkerSystem(): Promise<void> {
|
||||
console.log("[worker] Worker system initialized - ready for job enqueueing");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PgBoss instance for direct job enqueueing.
|
||||
* Must call initializeWorkerSystem() first.
|
||||
*/
|
||||
export function getBoss(): PgBoss {
|
||||
return boss;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
// if (Bun.env.NODE_ENV === "development") {
|
||||
// setupEventDebugger({ processLabel: "WORKER" });
|
||||
@@ -155,14 +147,32 @@ if (import.meta.main) {
|
||||
console.log(
|
||||
`[worker] Connecting to PgBoss on DATABASE_URL and SocketIO on ${process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671"}`
|
||||
);
|
||||
console.log(`[worker] DATABASE_URL set: ${!!process.env.DATABASE_URL}`);
|
||||
|
||||
// Ensure PgBoss is connected and queues exist
|
||||
await ensureBossStarted();
|
||||
console.log("[worker] Starting PgBoss...");
|
||||
try {
|
||||
await Promise.race([
|
||||
ensureBossStarted(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("boss.start() timed out after 30s")), 30_000)
|
||||
),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error("[worker] FATAL: PgBoss failed to start:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("[worker] PgBoss started successfully");
|
||||
console.log("[worker] Ensuring sync queues...");
|
||||
await ensureDalpuriSyncQueue();
|
||||
console.log("[worker] Sync queues ready");
|
||||
|
||||
// Register job handler for DALPURI_FULL_SYNC
|
||||
console.log("[worker] Importing sync-manager...");
|
||||
const { enqueueDalpuriFullSync } = await import("./modules/workers/sync-manager");
|
||||
console.log("[worker] Importing dalpuri-sync...");
|
||||
const { executeIncrementalSync } = await import("./modules/workers/dalpuri-sync");
|
||||
console.log("[worker] Importing incremental-sync...");
|
||||
const { enqueueIncrementalSync } = await import("./modules/workers/incremental-sync");
|
||||
await boss.work(WorkerQueue.DALPURI_FULL_SYNC, async () => {
|
||||
const socket = await ensureManagerSocketReady();
|
||||
|
||||
@@ -621,7 +621,7 @@
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||
|
||||
@@ -841,7 +841,7 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user