chore: initial Vector 2.0 monorepo
Ground-up TypeScript rewrite of the Vector hardware parts inventory
system. Ships the full roadmap (Phases 0-8) in one initial commit:
- pnpm + Turbo monorepo: apps/{api,web,e2e}, packages/{db,shared,ui,config}
- Express 5 + Prisma 5 + zod validation + JWT w/ refresh-token rotation
- React 19 + Vite + shadcn/ui + TanStack Query/Table + nuqs URL state
- Repair/RMA, tags, bulk ops, saved views, CSV audit export
- Analytics dashboard on Recharts + EOL tracking
- Signed webhook subscriptions (HMAC-SHA256) with in-process emitter
- Vitest unit tests (shared schemas, api services/helpers) + Playwright skeleton
- Gitea Actions CI (lint, typecheck, test+coverage, build) + Renovate
Deferred follow-ups: Postgres cutover (data-migration script ready),
BullMQ worker for webhook delivery, @react-pdf PDF export, CSV import wizard.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import * as svc from '../services/analytics.js';
|
||||
|
||||
export async function dashboard(_req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const data = await prisma.$transaction((tx) => svc.dashboard(tx));
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { csvCell } from './audit-export.js';
|
||||
|
||||
describe('csvCell', () => {
|
||||
it('returns empty string for null / undefined', () => {
|
||||
expect(csvCell(null)).toBe('');
|
||||
expect(csvCell(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('stringifies primitives', () => {
|
||||
expect(csvCell('hi')).toBe('hi');
|
||||
expect(csvCell(42)).toBe('42');
|
||||
expect(csvCell(true)).toBe('true');
|
||||
});
|
||||
|
||||
it('formats Date as ISO string', () => {
|
||||
const d = new Date('2026-01-01T00:00:00.000Z');
|
||||
expect(csvCell(d)).toBe('2026-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('quotes values containing commas', () => {
|
||||
expect(csvCell('a,b')).toBe('"a,b"');
|
||||
});
|
||||
|
||||
it('quotes values containing newlines', () => {
|
||||
expect(csvCell('line1\nline2')).toBe('"line1\nline2"');
|
||||
expect(csvCell('line1\r\nline2')).toBe('"line1\r\nline2"');
|
||||
});
|
||||
|
||||
it('escapes embedded double-quotes by doubling them', () => {
|
||||
expect(csvCell('say "hi"')).toBe('"say ""hi"""');
|
||||
});
|
||||
|
||||
it('leaves plain text untouched', () => {
|
||||
expect(csvCell('plain-text_123')).toBe('plain-text_123');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { prisma, Prisma } from '@vector/db';
|
||||
import { PartEventType } from '@vector/shared';
|
||||
|
||||
const Query = z.object({
|
||||
from: z.coerce.date().optional(),
|
||||
to: z.coerce.date().optional(),
|
||||
type: PartEventType.optional(),
|
||||
partId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
const HEADERS = [
|
||||
'createdAt',
|
||||
'eventType',
|
||||
'partId',
|
||||
'serialNumber',
|
||||
'field',
|
||||
'oldValue',
|
||||
'newValue',
|
||||
'actorUsername',
|
||||
];
|
||||
|
||||
// CSV-escape: wrap in quotes, double up embedded quotes. Handles commas, newlines, quotes.
|
||||
export function csvCell(value: unknown): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
const str = value instanceof Date ? value.toISOString() : String(value);
|
||||
if (/["\n\r,]/.test(str)) return `"${str.replace(/"/g, '""')}"`;
|
||||
return str;
|
||||
}
|
||||
|
||||
export async function eventsCsv(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const parsed = Query.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
code: 'VALIDATION_FAILED',
|
||||
message: 'Invalid export filters',
|
||||
issues: parsed.error.issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { from, to, type, partId } = parsed.data;
|
||||
|
||||
const where: Prisma.PartEventWhereInput = {};
|
||||
if (type) where.type = type;
|
||||
if (partId) where.partId = partId;
|
||||
if (from || to) {
|
||||
where.createdAt = {};
|
||||
if (from) where.createdAt.gte = from;
|
||||
if (to) where.createdAt.lte = to;
|
||||
}
|
||||
|
||||
res.setHeader('content-type', 'text/csv; charset=utf-8');
|
||||
res.setHeader(
|
||||
'content-disposition',
|
||||
`attachment; filename="vector-audit-${new Date().toISOString().slice(0, 10)}.csv"`,
|
||||
);
|
||||
res.setHeader('cache-control', 'no-store');
|
||||
res.write(HEADERS.join(',') + '\n');
|
||||
|
||||
// Keyset-paginate by createdAt+id so we never materialize the full table in memory.
|
||||
const BATCH = 1000;
|
||||
let cursor: { id: string } | undefined;
|
||||
for (;;) {
|
||||
const rows = await prisma.partEvent.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
||||
take: BATCH,
|
||||
...(cursor ? { skip: 1, cursor } : {}),
|
||||
include: {
|
||||
part: { select: { serialNumber: true } },
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
});
|
||||
if (rows.length === 0) break;
|
||||
for (const row of rows) {
|
||||
res.write(
|
||||
[
|
||||
csvCell(row.createdAt),
|
||||
csvCell(row.type),
|
||||
csvCell(row.partId),
|
||||
csvCell(row.part.serialNumber),
|
||||
csvCell(row.field),
|
||||
csvCell(row.oldValue),
|
||||
csvCell(row.newValue),
|
||||
csvCell(row.user?.username ?? null),
|
||||
].join(',') + '\n',
|
||||
);
|
||||
}
|
||||
if (rows.length < BATCH) break;
|
||||
const last = rows[rows.length - 1];
|
||||
if (!last) break;
|
||||
cursor = { id: last.id };
|
||||
}
|
||||
res.end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { CookieOptions, NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type { LoginRequest } from '@vector/shared';
|
||||
import { env } from '../env.js';
|
||||
import * as authService from '../services/auth.js';
|
||||
import { issueCsrfToken } from '../middleware/csrf.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
const accessCookieOpts: CookieOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: authService.ACCESS_TOKEN_TTL_MS,
|
||||
};
|
||||
|
||||
const refreshCookieOpts: CookieOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: env.NODE_ENV === 'production',
|
||||
path: '/api/auth',
|
||||
maxAge: authService.REFRESH_TOKEN_TTL_MS,
|
||||
};
|
||||
|
||||
function setAuthCookies(res: Response, tokens: authService.AuthTokens) {
|
||||
res.cookie('token', tokens.accessToken, accessCookieOpts);
|
||||
res.cookie('refresh', tokens.refreshToken, refreshCookieOpts);
|
||||
issueCsrfToken(res);
|
||||
}
|
||||
|
||||
function clearAuthCookies(res: Response) {
|
||||
res.clearCookie('token', { path: '/' });
|
||||
res.clearCookie('refresh', { path: '/api/auth' });
|
||||
res.clearCookie('csrf', { path: '/' });
|
||||
}
|
||||
|
||||
export async function login(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { username, password } = req.validated!.body as LoginRequest;
|
||||
const { user, tokens } = await prisma.$transaction((tx) =>
|
||||
authService.login(tx, username, password),
|
||||
);
|
||||
setAuthCookies(res, tokens);
|
||||
res.json(user);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function refresh(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const presented = req.cookies?.refresh;
|
||||
if (!presented) throw errors.unauthorized('Missing refresh token');
|
||||
const { user, tokens } = await prisma.$transaction((tx) =>
|
||||
authService.rotate(tx, presented),
|
||||
);
|
||||
setAuthCookies(res, tokens);
|
||||
res.json(user);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const presented = req.cookies?.refresh;
|
||||
await prisma.$transaction((tx) => authService.revoke(tx, presented));
|
||||
clearAuthCookies(res);
|
||||
res.json({ message: 'Logged out' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function me(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.user!.id } });
|
||||
if (!user) throw errors.unauthorized();
|
||||
res.json({ id: user.id, username: user.username, email: user.email, role: user.role });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '@vector/db';
|
||||
import {
|
||||
CreateBinRequest,
|
||||
PaginationQuery,
|
||||
UpdateBinRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/locations.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
const BinListQuery = PaginationQuery.extend({
|
||||
roomId: z.string().uuid().optional(),
|
||||
siteId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as z.infer<typeof BinListQuery>;
|
||||
const result = await prisma.$transaction((tx) => svc.listBins(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const bin = await prisma.$transaction((tx) => svc.getBin(tx, req.params.id));
|
||||
if (!bin) throw errors.notFound('Bin');
|
||||
res.json(bin);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateBinRequest;
|
||||
const bin = await prisma.$transaction((tx) => svc.createBin(tx, input));
|
||||
res.status(201).json(bin);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateBinRequest;
|
||||
const bin = await prisma.$transaction((tx) => svc.updateBin(tx, req.params.id, input));
|
||||
res.json(bin);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.removeBin(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export { BinListQuery };
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CategoryListQuery,
|
||||
CreateCategoryRequest,
|
||||
UpdateCategoryRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/categories.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as CategoryListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateCategoryRequest;
|
||||
const category = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(category);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateCategoryRequest;
|
||||
const category = await prisma.$transaction((tx) =>
|
||||
svc.update(tx, req.params.id, input),
|
||||
);
|
||||
res.json(category);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateHostRequest,
|
||||
HostListQuery,
|
||||
UpdateHostRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/hosts.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as HostListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const host = await prisma.$transaction((tx) => svc.get(tx, req.params.id));
|
||||
if (!host) throw errors.notFound('Host');
|
||||
res.json(host);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateHostRequest;
|
||||
const host = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(host);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateHostRequest;
|
||||
const host = await prisma.$transaction((tx) => svc.update(tx, req.params.id, input));
|
||||
res.json(host);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateManufacturerRequest,
|
||||
PaginationQuery,
|
||||
UpdateManufacturerRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/manufacturers.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = (req.validated!.query as PaginationQuery);
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateManufacturerRequest;
|
||||
const m = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(m);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateManufacturerRequest;
|
||||
const m = await prisma.$transaction((tx) => svc.update(tx, req.params.id, input));
|
||||
res.json(m);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export { errors };
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
BulkPartsRequest,
|
||||
CreatePartRequest,
|
||||
PartEventsQuery,
|
||||
PartListQuery,
|
||||
UpdatePartRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/parts.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as PartListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const part = await prisma.$transaction((tx) => svc.get(tx, req.params.id));
|
||||
if (!part) throw errors.notFound('Part');
|
||||
res.json(part);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreatePartRequest;
|
||||
const part = await prisma.$transaction((tx) =>
|
||||
svc.create(tx, input, req.user ?? null),
|
||||
);
|
||||
res.status(201).json(part);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdatePartRequest;
|
||||
const part = await prisma.$transaction((tx) =>
|
||||
svc.update(tx, req.params.id, input, req.user ?? null),
|
||||
);
|
||||
res.json(part);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulk(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as BulkPartsRequest;
|
||||
const result = await prisma.$transaction((tx) =>
|
||||
svc.bulkUpdate(tx, input, req.user ?? null),
|
||||
);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvents(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as PartEventsQuery;
|
||||
const result = await prisma.$transaction((tx) =>
|
||||
svc.listEvents(tx, req.params.id, q),
|
||||
);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateRepairJobRequest,
|
||||
RepairJobListQuery,
|
||||
UpdateRepairJobRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/repairs.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as RepairJobListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const repair = await prisma.$transaction((tx) => svc.get(tx, req.params.id));
|
||||
if (!repair) throw errors.notFound('Repair');
|
||||
res.json(repair);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listForPart(
|
||||
req: Request<{ id: string }>,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
try {
|
||||
const repairs = await prisma.$transaction((tx) => svc.listForPart(tx, req.params.id));
|
||||
res.json(repairs);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateRepairJobRequest;
|
||||
const repair = await prisma.$transaction((tx) =>
|
||||
svc.create(tx, input, req.user ?? null),
|
||||
);
|
||||
res.status(201).json(repair);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateRepairJobRequest;
|
||||
const repair = await prisma.$transaction((tx) =>
|
||||
svc.update(tx, req.params.id, input, req.user ?? null),
|
||||
);
|
||||
res.json(repair);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '@vector/db';
|
||||
import {
|
||||
CreateRoomRequest,
|
||||
PaginationQuery,
|
||||
UpdateRoomRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/locations.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
const RoomListQuery = PaginationQuery.extend({ siteId: z.string().uuid().optional() });
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as z.infer<typeof RoomListQuery>;
|
||||
const result = await prisma.$transaction((tx) => svc.listRooms(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const room = await prisma.$transaction((tx) => svc.getRoom(tx, req.params.id));
|
||||
if (!room) throw errors.notFound('Room');
|
||||
res.json(room);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateRoomRequest;
|
||||
const room = await prisma.$transaction((tx) => svc.createRoom(tx, input));
|
||||
res.status(201).json(room);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateRoomRequest;
|
||||
const room = await prisma.$transaction((tx) => svc.updateRoom(tx, req.params.id, input));
|
||||
res.json(room);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.removeRoom(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export { RoomListQuery };
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateSavedViewRequest,
|
||||
SavedViewListQuery,
|
||||
UpdateSavedViewRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/saved-views.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!req.user) throw errors.unauthorized();
|
||||
const q = req.validated!.query as SavedViewListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.listMine(tx, req.user!, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!req.user) throw errors.unauthorized();
|
||||
const input = req.validated!.body as CreateSavedViewRequest;
|
||||
const view = await prisma.$transaction((tx) => svc.create(tx, req.user!, input));
|
||||
res.status(201).json(view);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!req.user) throw errors.unauthorized();
|
||||
const input = req.validated!.body as UpdateSavedViewRequest;
|
||||
const view = await prisma.$transaction((tx) =>
|
||||
svc.update(tx, req.user!, req.params.id, input),
|
||||
);
|
||||
res.json(view);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!req.user) throw errors.unauthorized();
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.user!, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateSiteRequest,
|
||||
PaginationQuery,
|
||||
UpdateSiteRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/locations.js';
|
||||
import { errors } from '../lib/http-error.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as PaginationQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.listSites(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const site = await prisma.$transaction((tx) => svc.getSite(tx, req.params.id));
|
||||
if (!site) throw errors.notFound('Site');
|
||||
res.json(site);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateSiteRequest;
|
||||
const site = await prisma.$transaction((tx) => svc.createSite(tx, input));
|
||||
res.status(201).json(site);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateSiteRequest;
|
||||
const site = await prisma.$transaction((tx) => svc.updateSite(tx, req.params.id, input));
|
||||
res.json(site);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.removeSite(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
AssignTagsRequest,
|
||||
CreateTagRequest,
|
||||
TagListQuery,
|
||||
UpdateTagRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/tags.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as TagListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateTagRequest;
|
||||
const tag = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(tag);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateTagRequest;
|
||||
const tag = await prisma.$transaction((tx) => svc.update(tx, req.params.id, input));
|
||||
res.json(tag);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listForPart(
|
||||
req: Request<{ id: string }>,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
try {
|
||||
const tags = await prisma.$transaction((tx) => svc.listForPart(tx, req.params.id));
|
||||
res.json(tags);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function assignToPart(
|
||||
req: Request<{ id: string }>,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
try {
|
||||
const input = req.validated!.body as AssignTagsRequest;
|
||||
const tags = await prisma.$transaction((tx) =>
|
||||
svc.assignToPart(tx, req.params.id, input, req.user ?? null),
|
||||
);
|
||||
res.json(tags);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unassignFromPart(
|
||||
req: Request<{ id: string; tagId: string }>,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
try {
|
||||
const tags = await prisma.$transaction((tx) =>
|
||||
svc.unassignFromPart(tx, req.params.id, req.params.tagId, req.user ?? null),
|
||||
);
|
||||
res.json(tags);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
PaginationQuery,
|
||||
UpdateUserRequest,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/users.js';
|
||||
|
||||
export async function listUsers(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as PaginationQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateUserRequest;
|
||||
const u = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(u);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateUserRequest;
|
||||
const u = await prisma.$transaction((tx) => svc.update(tx, req.params.id, input));
|
||||
res.json(u);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import { prisma } from '@vector/db';
|
||||
import type {
|
||||
CreateWebhookSubscriptionRequest,
|
||||
UpdateWebhookSubscriptionRequest,
|
||||
WebhookSubscriptionListQuery,
|
||||
} from '@vector/shared';
|
||||
import * as svc from '../services/webhooks.js';
|
||||
|
||||
export async function list(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const q = req.validated!.query as WebhookSubscriptionListQuery;
|
||||
const result = await prisma.$transaction((tx) => svc.list(tx, q));
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as CreateWebhookSubscriptionRequest;
|
||||
const sub = await prisma.$transaction((tx) => svc.create(tx, input));
|
||||
res.status(201).json(sub);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const input = req.validated!.body as UpdateWebhookSubscriptionRequest;
|
||||
const sub = await prisma.$transaction((tx) => svc.update(tx, req.params.id, input));
|
||||
res.json(sub);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await prisma.$transaction((tx) => svc.remove(tx, req.params.id));
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function rotate(req: Request<{ id: string }>, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const sub = await prisma.$transaction((tx) => svc.rotateSecret(tx, req.params.id));
|
||||
res.json(sub);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user