Skip to content
Open
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
13 changes: 7 additions & 6 deletions lib/db/db-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,25 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
]

// Emit FILE_CREATED for new path
state.events.push({
const createdEvent: FileServerEvent = {
event_id: (state.idCounter + 0).toString(),
event_type: "FILE_CREATED",
file_path: normNew,
created_at: new Date().toISOString(),
initiator: opts.initiator,
})
}
// Emit FILE_DELETED for old path
state.events.push({
const deletedEvent: FileServerEvent = {
event_id: (state.idCounter + 1).toString(),
event_type: "FILE_DELETED",
file_path: normOld,
created_at: new Date().toISOString(),
initiator: opts.initiator,
})
}

return {
files,
events: state.events,
events: [...state.events, createdEvent, deletedEvent],
idCounter: state.idCounter + 2,
}
})
Expand Down Expand Up @@ -192,7 +192,8 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
let events = state.events

if (since) {
events = events.filter((e) => e.created_at > since)
const sinceTime = Date.parse(since)
events = events.filter((e) => Date.parse(e.created_at) > sinceTime)
}

if (event_type) {
Expand Down
7 changes: 7 additions & 0 deletions routes/files/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export default withRouteSpec({
})(async (req, ctx) => {
const { file_id, file_path, initiator } = req.commonParams

if (file_id && file_path) {
return ctx.json(
{ error: "Provide only one of file_id or file_path" },
{ status: 400 },
)
}

if (!file_id && !file_path) {
return ctx.json(
{ error: "Either file_id or file_path must be provided" },
Expand Down
4 changes: 4 additions & 0 deletions routes/files/rename.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"
import { normalizePath } from "lib/utils/normalize-path"

export default withRouteSpec({
methods: ["POST"],
Expand All @@ -21,6 +22,9 @@ export default withRouteSpec({
}),
})(async (req, ctx) => {
const body = await req.json()
if (!normalizePath(body.new_file_path)) {
return new Response("new_file_path must name a file", { status: 400 })
}

// First check if the old file exists
const oldFile = ctx.db.getFileByPath(body.old_file_path)
Expand Down
4 changes: 4 additions & 0 deletions routes/files/upsert.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import { z } from "zod"
import { normalizePath } from "lib/utils/normalize-path"

export default withRouteSpec({
methods: ["POST"],
Expand Down Expand Up @@ -31,6 +32,9 @@ export default withRouteSpec({
}),
})(async (req, ctx) => {
const body = await req.json()
if (!normalizePath(body.file_path)) {
return new Response("file_path must name a file", { status: 400 })
}
const file = ctx.db.upsertFile(body, { initiator: body.initiator })
return ctx.json({ file })
})
23 changes: 23 additions & 0 deletions tests/rename-events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from "bun:test"
import { createDatabase } from "lib/db/db-client"

test("rename preserves previous events snapshots", () => {
const db = createDatabase()
db.upsertFile(
{
file_path: "before.txt",
text_content: "content",
created_at: "2025-01-01T00:00:00Z",
},
{},
)
const previous = db.events
const snapshot = [...previous]
db.renameFile("before.txt", "after.txt", {})
expect(previous).toEqual(snapshot)
expect(db.events).not.toBe(previous)
expect(db.events.slice(-2).map((event) => event.event_type)).toEqual([
"FILE_CREATED",
"FILE_DELETED",
])
})
31 changes: 31 additions & 0 deletions tests/routes/empty-file-path.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { test, expect } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

test("creating or renaming a file to the root is rejected", async () => {
const { axios } = await getTestServer()
for (const file_path of ["", "/"]) {
await expect(
axios.post("/files/upsert", {
file_path,
text_content: "unreachable",
}),
).rejects.toMatchObject({ status: 400 })
}
await axios.post("/files/upsert", {
file_path: "valid.txt",
text_content: "retained",
})
await expect(
axios.post("/files/rename", {
old_file_path: "valid.txt",
new_file_path: "/",
}),
).rejects.toMatchObject({ status: 400 })
expect(
(
await axios.get("/files/get", {
params: { file_path: "valid.txt" },
})
).data.file.text_content,
).toBe("retained")
})
25 changes: 25 additions & 0 deletions tests/routes/event-timestamp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test, expect } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

test("since compares instants rather than timestamp strings", async () => {
const { axios } = await getTestServer()
const created = await axios.post("/events/create", {
event_type: "FILE_UPDATED",
file_path: "test.txt",
})
const instant = Date.parse(created.data.event.created_at)
const earlierOffset = new Date(instant + 2 * 3600000 - 1)
.toISOString()
.replace("Z", "+02:00")
const equalOffset = new Date(instant - 3600000)
.toISOString()
.replace("Z", "-01:00")
const later = await axios.get("/events/list", {
params: { since: earlierOffset },
})
expect(later.data.event_list).toHaveLength(1)
const equal = await axios.get("/events/list", {
params: { since: equalOffset },
})
expect(equal.data.event_list).toHaveLength(0)
})
22 changes: 22 additions & 0 deletions tests/routes/selector-conflict.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test, expect } from "bun:test"
import { getTestServer } from "tests/fixtures/get-test-server"

test("a conflicting delete selector cannot remove two files", async () => {
const { axios } = await getTestServer()
const first = await axios.post("/files/upsert", {
file_path: "first.txt",
text_content: "first",
})
await axios.post("/files/upsert", {
file_path: "second.txt",
text_content: "second",
})
await expect(
axios.post("/files/delete", {
file_id: first.data.file.file_id,
file_path: "second.txt",
}),
).rejects.toMatchObject({ status: 400 })
expect((await axios.get("/files/list")).data.file_list).toHaveLength(2)
expect((await axios.get("/events/list")).data.event_list).toHaveLength(2)
})
Loading