feat(db): enforce globally unique issue prefixes and identifiers
Derive issue_prefix from first 3 letters of company name with deterministic suffixes on collision. Migration rebuilds existing prefixes, reassigns issue numbers, and adds unique indexes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { eq, sql, count } from "drizzle-orm";
|
||||
import { eq, count } from "drizzle-orm";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import {
|
||||
companies,
|
||||
@@ -25,6 +25,50 @@ import {
|
||||
} from "@paperclip/db";
|
||||
|
||||
export function companyService(db: Db) {
|
||||
const ISSUE_PREFIX_FALLBACK = "CMP";
|
||||
|
||||
function deriveIssuePrefixBase(name: string) {
|
||||
const normalized = name.toUpperCase().replace(/[^A-Z]/g, "");
|
||||
return normalized.slice(0, 3) || ISSUE_PREFIX_FALLBACK;
|
||||
}
|
||||
|
||||
function suffixForAttempt(attempt: number) {
|
||||
if (attempt <= 1) return "";
|
||||
return "A".repeat(attempt - 1);
|
||||
}
|
||||
|
||||
function isIssuePrefixConflict(error: unknown) {
|
||||
const constraint = typeof error === "object" && error !== null && "constraint" in error
|
||||
? (error as { constraint?: string }).constraint
|
||||
: typeof error === "object" && error !== null && "constraint_name" in error
|
||||
? (error as { constraint_name?: string }).constraint_name
|
||||
: undefined;
|
||||
return typeof error === "object"
|
||||
&& error !== null
|
||||
&& "code" in error
|
||||
&& (error as { code?: string }).code === "23505"
|
||||
&& constraint === "companies_issue_prefix_idx";
|
||||
}
|
||||
|
||||
async function createCompanyWithUniquePrefix(data: typeof companies.$inferInsert) {
|
||||
const base = deriveIssuePrefixBase(data.name);
|
||||
let suffix = 1;
|
||||
while (suffix < 10000) {
|
||||
const candidate = `${base}${suffixForAttempt(suffix)}`;
|
||||
try {
|
||||
const rows = await db
|
||||
.insert(companies)
|
||||
.values({ ...data, issuePrefix: candidate })
|
||||
.returning();
|
||||
return rows[0];
|
||||
} catch (error) {
|
||||
if (!isIssuePrefixConflict(error)) throw error;
|
||||
}
|
||||
suffix += 1;
|
||||
}
|
||||
throw new Error("Unable to allocate unique issue prefix");
|
||||
}
|
||||
|
||||
return {
|
||||
list: () => db.select().from(companies),
|
||||
|
||||
@@ -35,12 +79,7 @@ export function companyService(db: Db) {
|
||||
.where(eq(companies.id, id))
|
||||
.then((rows) => rows[0] ?? null),
|
||||
|
||||
create: (data: typeof companies.$inferInsert) =>
|
||||
db
|
||||
.insert(companies)
|
||||
.values(data)
|
||||
.returning()
|
||||
.then((rows) => rows[0]),
|
||||
create: async (data: typeof companies.$inferInsert) => createCompanyWithUniquePrefix(data),
|
||||
|
||||
update: (id: string, data: Partial<typeof companies.$inferInsert>) =>
|
||||
db
|
||||
|
||||
Reference in New Issue
Block a user