Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 4 additions & 5 deletions .github/workflows/typescript-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ jobs:
- name: Install packages
run: npm ci

- name: Check formatting
run: npm run format:check

- name: Lint
run: npm run lint
# `ultracite check` runs Biome's combined formatter + linter check
# (read-only), replacing the separate Prettier `--check` + ESLint steps.
- name: Lint and format check
run: npm run lint:check

- name: Type check
run: npm run typecheck
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ recreate it from the "Minimal `.env`" block in `docs/local-development.md` (the
writes to `web/.next` and **contends with a running `make dev`** (also using `.next`). Stop the
dev server before running integration tests, then restart it afterward.
- Lint/format/typecheck: `make check-all` (note: `py-format`/`fe-format` auto-rewrite files; use
`uv run ruff check .`, `npm run lint`, `npm run typecheck`, `npm run format:check` for read-only checks).
`uv run ruff check .`, `npm run lint:check` (Biome formatter + linter, read-only), and
`npm run typecheck` for read-only checks).
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ py-check-watcher-version:
# Web app.
.PHONY: fe-format
fe-format:
cd web && npm run format
cd web && npm run lint:fix

.PHONY: fe-lint
fe-lint:
cd web && npm run lint
cd web && npm run lint:check

.PHONY: fe-typecheck
fe-typecheck:
Expand Down
5 changes: 2 additions & 3 deletions docs/ci-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ Four workflows run on pushes to `staging`/`production` and on pull requests targ
### TypeScript lint and typecheck (`typescript-lint.yml`)

1. Install dependencies with `npm ci`.
2. `npm run format:check` — Prettier.
3. `npm run lint` — ESLint.
4. `npm run typecheck` — TypeScript compiler.
2. `npm run lint:check` — Biome (via Ultracite), combined formatter + linter check.
3. `npm run typecheck` — TypeScript compiler.

### TypeScript tests (`typescript-test.yml`)

Expand Down
3 changes: 1 addition & 2 deletions docs/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ Environment-specific configuration is managed through environment variables, nev

### TypeScript / JavaScript

- Formatter: [Prettier](https://prettier.io/)
- Linter: [ESLint](https://eslint.org/)
- Formatter + linter: [Biome](https://biomejs.dev/) via [Ultracite](https://www.ultracite.ai/) (`npm run lint:check` / `lint:fix`)
- Type checker: TypeScript compiler (`tsc`)

### Pre-commit
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ This runs both Python and web app checks:
| `make py-format` | Auto-fix with Ruff |
| `make py-lint` | Ruff linter |
| `make py-typecheck` | Pyright |
| `make fe-format` | Prettier |
| `make fe-lint` | ESLint |
| `make fe-format` | Biome format + safe lint fixes (`ultracite fix`) |
| `make fe-lint` | Biome format + lint check (`ultracite check`) |
| `make fe-typecheck` | TypeScript compiler |

## Running tests
Expand Down
7 changes: 0 additions & 7 deletions web/.prettierignore

This file was deleted.

11 changes: 0 additions & 11 deletions web/.prettierrc

This file was deleted.

54 changes: 54 additions & 0 deletions web/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"emmet.showExpandedAbbreviation": "never",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[graphql]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[html]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[mdx]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[svelte]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[vue]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[yaml]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
15 changes: 7 additions & 8 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ This directory contains the source code for the Data Hub web application and API
| `npm run dev` | Start dev server (Turbopack) |
| `npm run build` | Production build |
| `npm run start` | Start production server |
| `npm run lint` | Run ESLint |
| `npm run format` | Format code with Prettier |
| `npm run format:check` | Check formatting without writing |
| `npm run lint:check` | Check formatting + lint with Biome (read-only) |
| `npm run lint:fix` | Format and apply safe lint fixes with Biome |
| `npm run typecheck` | Run the TypeScript compiler (no emit) |
| `npm run precommit` | Format + lint + typecheck (run before committing) |
| `npm run check` | Lint check + typecheck |
| `npm run precommit` | Lint fix + typecheck (run before committing) |
| `npm run db:generate` | Generate Drizzle migration files |
| `npm run db:migrate` | Apply pending migrations |
| `npm run db:push` | Push schema to database (no migration files) |
Expand Down Expand Up @@ -86,11 +86,10 @@ See the table below for a summary of environment variables configured for this a

## CI

A GitHub Actions workflow (`.github/workflows/typescript-lint.yml`) runs on every push to `staging` and `production`, as well as pull requests targeting both branches. It executes three checks:
A GitHub Actions workflow (`.github/workflows/typescript-lint.yml`) runs on every push to `staging` and `production`, as well as pull requests targeting both branches. It executes two checks:

1. **Format check** — `npm run format:check` (Prettier)
2. **Lint** — `npm run lint` (ESLint)
3. **Type check** — `npm run typecheck` (TypeScript compiler)
1. **Lint and format check** — `npm run lint:check` (Biome, via Ultracite — combined formatter + linter, read-only)
2. **Type check** — `npm run typecheck` (TypeScript compiler)

Run `npm run precommit` locally before pushing to catch the same issues earlier.

Expand Down
28 changes: 17 additions & 11 deletions web/app/api/local-s3/[bucket]/[...key]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,30 @@
// on the Edge runtime and there's no production deployment story for
// this route anyway.

import {
getLocalMirrorRoot,
mimeFor,
resolveMirrorPath,
} from "@/lib/s3-local-mirror";
import type { NextRequest } from "next/server";
import { createReadStream, createWriteStream } from "node:fs";
import { mkdir, stat } from "node:fs/promises";
import path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import type { ReadableStream as NodeWebReadableStream } from "node:stream/web";
import type { NextRequest } from "next/server";
import {
getLocalMirrorRoot,
mimeFor,
resolveMirrorPath,
} from "@/lib/s3-local-mirror";

type RouteContext = {
interface RouteContext {
params: Promise<{ bucket: string; key: string[] }>;
};
}

const NOT_FOUND_RESPONSE = () => new Response("Not Found", { status: 404 });

export async function GET(request: NextRequest, { params }: RouteContext) {
const root = getLocalMirrorRoot();
if (!root) return NOT_FOUND_RESPONSE();
if (!root) {
return NOT_FOUND_RESPONSE();
}

const { bucket, key } = await params;
const joinedKey = key.join("/");
Expand All @@ -50,7 +52,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) {
let fileSize: number;
try {
const s = await stat(filePath);
if (!s.isFile()) return NOT_FOUND_RESPONSE();
if (!s.isFile()) {
return NOT_FOUND_RESPONSE();
}
fileSize = s.size;
} catch {
return NOT_FOUND_RESPONSE();
Expand Down Expand Up @@ -79,7 +83,9 @@ export async function GET(request: NextRequest, { params }: RouteContext) {

export async function PUT(request: NextRequest, { params }: RouteContext) {
const root = getLocalMirrorRoot();
if (!root) return NOT_FOUND_RESPONSE();
if (!root) {
return NOT_FOUND_RESPONSE();
}

const { bucket, key } = await params;
const joinedKey = key.join("/");
Expand Down
41 changes: 21 additions & 20 deletions web/app/api/v1/archive-jobs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { authorize } from "@/lib/api/auth";
import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors";
import { isValidUUID } from "@/lib/api/validators";
import { db } from "@/lib/db";
import { archiveJobs } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";

type RouteContext = {
interface RouteContext {
params: Promise<{ id: string }>;
};
}

// ---------------------------------------------------------------------------
// PATCH /api/v1/archive-jobs/:id
Expand All @@ -31,17 +31,19 @@ type RouteContext = {

const TERMINAL_STATUSES = new Set(["ready", "failed"]);

type PatchBody = {
status?: unknown;
interface PatchBody {
archive_bucket?: unknown;
archive_key?: unknown;
size_bytes?: unknown;
error_message?: unknown;
};
size_bytes?: unknown;
status?: unknown;
}

export async function PATCH(request: NextRequest, { params }: RouteContext) {
const authResult = await authorize(request, "archive-jobs:write");
if (authResult instanceof Response) return authResult;
if (authResult instanceof Response) {
return authResult;
}

const { id } = await params;
if (!isValidUUID(id)) {
Expand All @@ -68,17 +70,16 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) {

const status = body.status as "pending" | "building" | "ready" | "failed";

if (status === "ready") {
if (
typeof body.archive_bucket !== "string" ||
typeof body.archive_key !== "string"
) {
return apiError(
400,
VALIDATION_ERROR,
"archive_bucket and archive_key are required when status is 'ready'"
);
}
if (
status === "ready" &&
(typeof body.archive_bucket !== "string" ||
typeof body.archive_key !== "string")
) {
return apiError(
400,
VALIDATION_ERROR,
"archive_bucket and archive_key are required when status is 'ready'"
);
}

const update: Partial<typeof archiveJobs.$inferInsert> = { status };
Expand Down
18 changes: 10 additions & 8 deletions web/app/api/v1/files/[fileId]/download/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { authorize } from "@/lib/api/auth";
import { apiError, NOT_FOUND, VALIDATION_ERROR } from "@/lib/api/errors";
import { db } from "@/lib/db";
import { files, instrumentRuns } from "@/lib/db/schema";
import { getPresignedDownloadUrl } from "@/lib/s3";
import { eq } from "drizzle-orm";
import type { NextRequest } from "next/server";

type RouteContext = {
interface RouteContext {
params: Promise<{ fileId: string }>;
};
}

// ---------------------------------------------------------------------------
// GET /api/v1/files/:fileId/download
Expand All @@ -20,11 +20,13 @@ type RouteContext = {

export async function GET(request: NextRequest, { params }: RouteContext) {
const authResult = await authorize(request, "files:read");
if (authResult instanceof Response) return authResult;
if (authResult instanceof Response) {
return authResult;
}

const { fileId } = await params;
const numericId = parseInt(fileId, 10);
if (isNaN(numericId)) {
const numericId = Number.parseInt(fileId, 10);
if (Number.isNaN(numericId)) {
return apiError(400, VALIDATION_ERROR, "Invalid file ID");
}

Expand Down Expand Up @@ -57,7 +59,7 @@ export async function GET(request: NextRequest, { params }: RouteContext) {
return apiError(404, NOT_FOUND, `File '${fileId}' not found`);
}

if (!file.s3Bucket || !file.s3Key) {
if (!(file.s3Bucket && file.s3Key)) {
return apiError(404, NOT_FOUND, "File has not been uploaded to S3 yet");
}

Expand Down
14 changes: 8 additions & 6 deletions web/app/api/v1/files/[fileId]/reprocess/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { NextRequest } from "next/server";
import { authorize } from "@/lib/api/auth";
import { apiError, VALIDATION_ERROR } from "@/lib/api/errors";
import { reprocessFile } from "@/lib/api/file-reprocessing";
import type { NextRequest } from "next/server";

type RouteContext = {
interface RouteContext {
params: Promise<{ fileId: string }>;
};
}

// ---------------------------------------------------------------------------
// POST /api/v1/files/:fileId/reprocess
Expand All @@ -18,11 +18,13 @@ type RouteContext = {

export async function POST(request: NextRequest, { params }: RouteContext) {
const authResult = await authorize(request, "files:write");
if (authResult instanceof Response) return authResult;
if (authResult instanceof Response) {
return authResult;
}

const { fileId } = await params;
const numericId = parseInt(fileId, 10);
if (isNaN(numericId)) {
const numericId = Number.parseInt(fileId, 10);
if (Number.isNaN(numericId)) {
return apiError(400, VALIDATION_ERROR, "Invalid file ID");
}

Expand Down
Loading