Skip to content
Merged

Bugs #28

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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.0
0.2.1
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dequel-api",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -10,6 +10,7 @@
},
"dependencies": {
"@elysiajs/cors": "^1.1.1",
"@sinclair/typebox": "^0.34.13",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -s https://registry.npmjs.org/elysia/1.1.26 | jq '.dependencies["`@sinclair/typebox`"]'
curl -s https://registry.npmjs.org/@sinclair/typebox | jq '."dist-tags"'

Repository: Lftobs/dequel

Length of output: 490


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== apps/api/package.json relevant dependencies =="
python3 - <<'PY'
import json, pathlib
p=pathlib.Path('apps/api/package.json')
if p.exists():
    data=json.loads(p.read_text())
    for sec in ['dependencies','devDependencies','peerDependencies']:
        vals={k:v for k,v in data.get(sec,{}).items() if k in {'`@sinclair/typebox`','elysia'} or k.startswith('`@sinclair/typebox`')}
        if vals:
            print(sec, vals)
PY

echo
echo "== elysia package files mentioning TypeBox references =="
git ls-files '*.js' '*.ts' 'apps/**' 'packages/**' 2>/dev/null | rg 'packages|apps' | head -100
echo
rg -n "Symbol\.for\('TypeBox\.Kind'\)|function TypeBox|TypeBox\.Kind|Symbol\.FOR|Elysia" package.json apps packages 2>/dev/null || true

echo
echo "== lockfile `@sinclair/typebox` resolution snippets =="
for f in package-lock.json pnpm-lock.yaml yarn.lock bun.lockb bun.lock apps/api/package-lock.json apps/api/pnpm-lock.yaml apps/api/yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -A2 -B2 '`@sinclair/typebox`|elysia' "$f" | head -200
  fi
done

echo
echo "== elysia 1.1.26 package registry summary =="
curl -s https://registry.npmjs.org/elysia/1.1.26 | jq '{version, dependencies, peerDependencies}'
echo
echo "== elysia 1.1.x `@sinclair/typebox` range across patch/minor if available =="
for patch in 1.1.0 1.1.1 1.1.10 1.1.20 1.1.26; do
  echo "--- $patch"
  curl -s "https://registry.npmjs.org/elysia/$patch" | jq '{version, dependencies, peerDependencies}' | rg '"`@sinclair/typebox`"|"Version|"Peer'
done

echo
echo "== local typebox import snippets =="
rg -n '"`@sinclair/typebox`"|from "`@sinclair/typebox`"|require\("`@sinclair/typebox`"\)' apps/api -S 2>/dev/null || true

Repository: Lftobs/dequel

Length of output: 7245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== bun.lock `@sinclair/typebox` entries =="
if [ -f apps/api/bun.lock ]; then
  rg -n -A3 -B3 '`@sinclair/typebox`' apps/api/bun.lock
else
  echo "apps/api/bun.lock not found"
fi

echo
echo "== npm elysia 1.1.26 package tarball TypeBox file references =="
tmp="$(mktemp -d)"
curl -sL https://registry.npmjs.org/elysia/-/elysia-1.1.26.tgz -o "$tmp/elysia.tgz"
tar -xzf "$tmp/elysia.tgz" -C "$tmp"
rg -n "TypeBox\.Kind|Symbol\.for|`@sinclair/typebox`" "$tmp/package"/dist "$tmp/package/src" 2>/dev/null | head -200 || true
echo
echo "== `@sinclair/typebox` 0.32.34 and 0.34.13 Kind symbol =="
for v in 0.32.34 0.34.13; do
  echo "--- $v"
  curl -sL "https://registry.npmjs.org/@sinclair%2ftypebox/-/typebox-$v.tgz" -o "$tmp/typebox-$v.tgz"
  tar -xzf "$tmp/typebox-$v.tgz" -C "$tmp"
  rg -n "Symbol\.for\('TypeBox\.Kind'\)|let Kind|exports\.Kind" "$tmp/package/src" | head -100
done

Repository: Lftobs/dequel

Length of output: 24922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"

echo "== elysia latest package summary =="
curl -s https://registry.npmjs.org/elysia/latest\?metadata=true | jq '{latest: ."dist-tags".latest, version: .version, dependencies: .dependencies, peerDependencies: .peerDependencies}'

echo
echo "== bun.lock package map around elysia/typebox/exact =="
python3 - <<'PY'
from pathlib import Path
p = Path('apps/api/bun.lock')
text = p.read_text(errors='replace').splitlines()
for i,line in enumerate(text,1):
    if any(pkg in line for pkg in ['`@sinclair/typebox`', 'elysia', 'exact-mirror']):
        start=max(1,i-2); end=min(len(text),i+3)
        print(f'[{i}] {line}')
        for n in range(start,end+1):
            if n!=i:
                print(f'[{n}] {text[n-1]}')
        print()
PY

echo
echo "== TypeBox 0.32.34 Kind export =="
curl -sL "https://registry.npmjs.org/@sinclair%2ftypebox/-/typebox-0.32.34.tgz" -o "$tmp/tbox-0.32.34.tgz"
tar -xzf "$tmp/tbox-0.32.34.tgz" -C "$tmp/0.32"
rg -n "KindSymbol|Symbol\.for\(\"TypeBox\.Kind\"\)|exports\.Kind|return .*KindSymbol" "$tmp/0.32/package" | head -50

echo
echo "== TypeBox 0.34.13 Kind export =="
curl -sL "https://registry.npmjs.org/@sinclair%2ftypebox/-/typebox-0.34.13.tgz" -o "$tmp/tbox-0.34.13.tgz"
tar -xzf "$tmp/tbox-0.34.13.tgz" -C "$tmp/0.34"
rg -n "KindSymbol|Symbol\.for\(\"TypeBox\.Kind\"\)|exports\.Kind|return .*KindSymbol" "$tmp/0.34/package" | head -50

echo
echo "== npm tarball source file names containing KindSymbol =="
for v in 0.32.34 0.34.13; do
  echo "--- $v"
  tar -tzf "$tmp/tbox-$v.tgz" | rg 'KindSymbol|value|index|system' | head -80
done

echo
echo "== TypeBox KindSymbol definition files =="
for file in "$tmp/0.32/package/src/type-system.ts" "$tmp/0.32/package/src/type.ts" "$tmp/0.34/package/src/type-system.ts" "$tmp/0.34/package/src/type.ts"; do
  if [ -f "$file" ]; then
    echo "--- $file"
    rg -n -C 5 "KindSymbol|Symbol\.for\(\"TypeBox\.Kind\"\)" "$file" || true
  fi
done

Repository: Lftobs/dequel

Length of output: 7105


Align @sinclair/typebox with the installed Elysia package.

apps/api/package.json declares elysia: ^1.1.26, but apps/api/bun.lock resolves elysia@1.4.28, whose peer dependency requires @sinclair/typebox >= 0.34.0 < 1. If you stay with Elysia 1.1.x, update the lockfile to match; if Elysia 1.4.x is intended, update the package range to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/package.json` at line 13, Align the Elysia and `@sinclair/typebox`
dependency declarations in apps/api: either update the lockfile to resolve
elysia within the declared ^1.1.26 range, or, if the resolved 1.4.x version is
intended, update the elysia range in package.json accordingly while retaining a
compatible `@sinclair/typebox` range.

"drizzle-orm": "^0.45.2",
"elysia": "^1.1.26",
"ioredis": "^5.4.1",
Expand Down
9 changes: 6 additions & 3 deletions apps/api/src/api/deployments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createDeployment,
countDeployments,
getDeploymentById,
getProjectById,
getLogs,
listDeployments,
} from "../../db/repo";
Expand Down Expand Up @@ -66,6 +67,7 @@ export const deploymentsRoutes = new Elysia()
String(form.get("commitSha") ?? "").trim() ||
undefined;
const clearCache = form.get("clearCache") === "true";
const resolvedBranch = branch || (projectId ? (await getProjectById(projectId))?.repoBranch || undefined : undefined);
if (
sourceType !== "git" &&
sourceType !== "upload" &&
Expand All @@ -88,7 +90,7 @@ export const deploymentsRoutes = new Elysia()
projectId,
sourceType: "git",
sourceRef: gitUrl,
branch,
branch: resolvedBranch,
environment,
commitSha,
clearCache,
Expand Down Expand Up @@ -122,7 +124,7 @@ export const deploymentsRoutes = new Elysia()
projectId,
sourceType: "upload",
sourceRef: uploadPath,
branch,
branch: resolvedBranch,
environment,
clearCache,
});
Expand Down Expand Up @@ -186,11 +188,12 @@ export const deploymentsRoutes = new Elysia()
error: "Cannot redeploy an image-based (rollback) deployment — rollback to an earlier source deployment instead",
};
}
const project = original.projectId ? await getProjectById(original.projectId) : null;
const deployment = await createDeployment({
projectId: original.projectId || undefined,
sourceType: original.sourceType,
sourceRef: original.sourceRef,
branch: original.branch || undefined,
branch: original.branch || project?.repoBranch || undefined,
environment: original.environment || undefined,
});
orchestrator.enqueue(deployment.id);
Expand Down
26 changes: 6 additions & 20 deletions apps/api/src/api/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,6 @@ export const projectsRoutes = new Elysia()
set.status = 400;
return { error: "name is required" };
}
if (body?.repoUrl) {
const projects = await listProjects();
const normalize = (u: string) => u.replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
const incoming = normalize(body.repoUrl);
const duplicate = projects.find((p) => p.repoUrl && normalize(p.repoUrl) === incoming);
if (duplicate) {
set.status = 409;
return { error: `A project with this repository URL already exists: "${duplicate.name}"` };
}
}
const project = await createProject({
name: body.name,
description: body.description,
Expand All @@ -55,23 +45,16 @@ export const projectsRoutes = new Elysia()
port: body.port ? Number(body.port) : null,
sourceDir: body.sourceDir || null,
sourceType: body.sourceType || "git",
projectType: body.projectType || "web",
buildCommand: body.buildCommand || undefined,
startCommand: body.startCommand || undefined,
});
return project;
},
)
.patch(
"/projects/:id",
async ({ params: { id }, body, set }: any) => {
if (body?.repoUrl) {
const projects = await listProjects();
const normalize = (u: string) => u.replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
const incoming = normalize(body.repoUrl);
const duplicate = projects.find((p) => p.id !== id && p.repoUrl && normalize(p.repoUrl) === incoming);
if (duplicate) {
set.status = 409;
return { error: `A project with this repository URL already exists: "${duplicate.name}"` };
}
}
const project = await updateProject(id, {
name: body?.name,
description: body?.description,
Expand All @@ -82,6 +65,9 @@ export const projectsRoutes = new Elysia()
memoryLimitMb: body?.memoryLimitMb,
port: body?.port ? Number(body.port) : body?.port === null ? null : undefined,
sourceDir: body?.sourceDir ?? undefined,
projectType: body?.projectType ?? undefined,
buildCommand: "buildCommand" in (body ?? {}) ? (body.buildCommand ?? "") || null : undefined,
startCommand: "startCommand" in (body ?? {}) ? (body.startCommand ?? "") || null : undefined,
Comment on lines +69 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard primitive PATCH bodies before using in.

Because body is any, a valid JSON string or number can reach this handler. Lines [69-70] then throw a TypeError because the right-hand operand of in must be an object, producing a 500 instead of a controlled 4xx response. Validate that body is a non-null object before these checks and add a regression test.

Suggested fix
 async ({ params: { id }, body, set }: any) => {
+  if (body === null || typeof body !== "object" || Array.isArray(body)) {
+    set.status = 400;
+    return { error: "body must be an object" };
+  }
+
   const project = await updateProject(id, {
...
-    buildCommand: "buildCommand" in (body ?? {}) ? ...
+    buildCommand: Object.prototype.hasOwnProperty.call(body, "buildCommand") ? ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
buildCommand: "buildCommand" in (body ?? {}) ? (body.buildCommand ?? "") || null : undefined,
startCommand: "startCommand" in (body ?? {}) ? (body.startCommand ?? "") || null : undefined,
async ({ params: { id }, body, set }: any) => {
if (body === null || typeof body !== "object" || Array.isArray(body)) {
set.status = 400;
return { error: "body must be an object" };
}
const project = await updateProject(id, {
buildCommand: Object.prototype.hasOwnProperty.call(body, "buildCommand") ? (body.buildCommand ?? "") || null : undefined,
startCommand: Object.prototype.hasOwnProperty.call(body, "startCommand") ? (body.startCommand ?? "") || null : undefined,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/api/projects/index.ts` around lines 69 - 70, Guard the PATCH
request body before the `"buildCommand" in ...` and `"startCommand" in ...`
checks: require a non-null object and return the handler’s established
controlled 4xx response for primitive or otherwise invalid bodies. Preserve the
existing command normalization for valid object bodies, and add a regression
test covering JSON string or number payloads.

});
if (!project) {
set.status = 404;
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/api/scaling/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
deleteScalingPolicy,
getScalingPolicy,
upsertScalingPolicy,
getProjectById,
} from "../../db/repo";

export const scalingRoutes = new Elysia()
Expand All @@ -24,6 +25,15 @@ export const scalingRoutes = new Elysia()
set.status = 400;
return { error: "body is required" };
}
const project = await getProjectById(params.id);
if (!project) {
set.status = 404;
return { error: "Project not found" };
}
if (body.enabled !== false && (!project.cpuLimit || project.cpuLimit <= 0)) {
set.status = 400;
return { error: "Cannot enable autoscaling on a project without CPU resource limits configured." };
}
return upsertScalingPolicy({
projectId: params.id,
...body,
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const migrate = async () => {
drizzleMigrate(db, { migrationsFolder });

await addClearCacheColumn(db);
await addFinishedAtColumn(db);
await seedFromConfig();
};

Expand All @@ -45,6 +46,18 @@ const addClearCacheColumn = async (db: ReturnType<typeof getDrizzle>) => {
}
};

const addFinishedAtColumn = async (db: ReturnType<typeof getDrizzle>) => {
try {
db.run(sql`ALTER TABLE deployments ADD COLUMN finished_at text`);
console.log("[Migrate] Added finished_at column to deployments table");
} catch (err) {
const cause = err instanceof Error && "cause" in err ? err.cause : err;
if (cause instanceof Error && cause.message.includes("duplicate column name")) return;
console.error("[Migrate] Failed to add finished_at column:", err);
throw err;
}
};

const seedFromConfig = async () => {
if (config.githubClientId && config.githubClientSecret) {
const existing = await getGithubIntegration();
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/db/migrations/0002_colossal_flatman.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `projects` ADD `project_type` text DEFAULT 'web' NOT NULL;
2 changes: 2 additions & 0 deletions apps/api/src/db/migrations/0003_aberrant_viper.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `projects` ADD `build_command` text;--> statement-breakpoint
ALTER TABLE `projects` ADD `start_command` text;
Loading
Loading