feat(11-01): schema, service, and tests for sort_order + reorderCandidates
- Add sortOrder REAL column to threadCandidates schema (default 0) - Add sort_order column to test helper CREATE TABLE - Add reorderCandidatesSchema to shared/schemas.ts - Add ReorderCandidates type to shared/types.ts - getThreadWithCandidates now orders candidates by sort_order ASC - createCandidate appends at max sort_order + 1000 (first = 1000) - Add reorderCandidates service function (transaction, active-only guard) - Add 5 new tests: ordering, appending, reorder success, resolved guard, missing thread
This commit is contained in:
@@ -61,6 +61,7 @@ export const threadCandidates = sqliteTable("thread_candidates", {
|
||||
status: text("status").notNull().default("researching"),
|
||||
pros: text("pros"),
|
||||
cons: text("cons"),
|
||||
sortOrder: real("sort_order").notNull().default(0),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { asc, desc, eq, max, sql } from "drizzle-orm";
|
||||
import { db as prodDb } from "../../db/index.ts";
|
||||
import {
|
||||
categories,
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
threadCandidates,
|
||||
threads,
|
||||
} from "../../db/schema.ts";
|
||||
import type { CreateCandidate, CreateThread } from "../../shared/types.ts";
|
||||
import type {
|
||||
CreateCandidate,
|
||||
CreateThread,
|
||||
ReorderCandidates,
|
||||
} from "../../shared/types.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
|
||||
@@ -83,6 +87,7 @@ export function getThreadWithCandidates(db: Db = prodDb, threadId: number) {
|
||||
.from(threadCandidates)
|
||||
.innerJoin(categories, eq(threadCandidates.categoryId, categories.id))
|
||||
.where(eq(threadCandidates.threadId, threadId))
|
||||
.orderBy(asc(threadCandidates.sortOrder))
|
||||
.all();
|
||||
|
||||
return { ...thread, candidates: candidateList };
|
||||
@@ -141,6 +146,14 @@ export function createCandidate(
|
||||
imageFilename?: string;
|
||||
},
|
||||
) {
|
||||
const maxRow = db
|
||||
.select({ maxOrder: max(threadCandidates.sortOrder) })
|
||||
.from(threadCandidates)
|
||||
.where(eq(threadCandidates.threadId, threadId))
|
||||
.get();
|
||||
|
||||
const nextSortOrder = (maxRow?.maxOrder ?? 0) + 1000;
|
||||
|
||||
return db
|
||||
.insert(threadCandidates)
|
||||
.values({
|
||||
@@ -155,6 +168,7 @@ export function createCandidate(
|
||||
status: data.status ?? "researching",
|
||||
pros: data.pros ?? null,
|
||||
cons: data.cons ?? null,
|
||||
sortOrder: nextSortOrder,
|
||||
})
|
||||
.returning()
|
||||
.get();
|
||||
@@ -203,6 +217,35 @@ export function deleteCandidate(db: Db = prodDb, candidateId: number) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function reorderCandidates(
|
||||
db: Db = prodDb,
|
||||
threadId: number,
|
||||
orderedIds: ReorderCandidates["orderedIds"],
|
||||
): { success: boolean; error?: string } {
|
||||
return db.transaction((tx) => {
|
||||
const thread = tx
|
||||
.select()
|
||||
.from(threads)
|
||||
.where(eq(threads.id, threadId))
|
||||
.get();
|
||||
if (!thread) {
|
||||
return { success: false, error: "Thread not found" };
|
||||
}
|
||||
if (thread.status !== "active") {
|
||||
return { success: false, error: "Thread not active" };
|
||||
}
|
||||
|
||||
for (let i = 0; i < orderedIds.length; i++) {
|
||||
tx.update(threadCandidates)
|
||||
.set({ sortOrder: (i + 1) * 1000 })
|
||||
.where(eq(threadCandidates.id, orderedIds[i]))
|
||||
.run();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveThread(
|
||||
db: Db = prodDb,
|
||||
threadId: number,
|
||||
|
||||
@@ -63,6 +63,10 @@ export const resolveThreadSchema = z.object({
|
||||
candidateId: z.number().int().positive(),
|
||||
});
|
||||
|
||||
export const reorderCandidatesSchema = z.object({
|
||||
orderedIds: z.array(z.number().int().positive()).min(1),
|
||||
});
|
||||
|
||||
// Setup schemas
|
||||
export const createSetupSchema = z.object({
|
||||
name: z.string().min(1, "Setup name is required"),
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
createItemSchema,
|
||||
createSetupSchema,
|
||||
createThreadSchema,
|
||||
reorderCandidatesSchema,
|
||||
resolveThreadSchema,
|
||||
syncSetupItemsSchema,
|
||||
updateCandidateSchema,
|
||||
@@ -33,6 +34,7 @@ export type UpdateThread = z.infer<typeof updateThreadSchema>;
|
||||
export type CreateCandidate = z.infer<typeof createCandidateSchema>;
|
||||
export type UpdateCandidate = z.infer<typeof updateCandidateSchema>;
|
||||
export type ResolveThread = z.infer<typeof resolveThreadSchema>;
|
||||
export type ReorderCandidates = z.infer<typeof reorderCandidatesSchema>;
|
||||
|
||||
// Setup types
|
||||
export type CreateSetup = z.infer<typeof createSetupSchema>;
|
||||
|
||||
Reference in New Issue
Block a user