ad155d6344
If a previous run stored the company name with literal quotes
('B4L'), the current run's name-based select missed the row and
the insert collided on the unique slug. Looking up by slug is the
natural idempotency key here: slug is derived deterministically
from name, so a new run and the corrupted row produce the same
slug and therefore resolve to the same company row. If the stored
name differs from the new one, heal it with an UPDATE and log the
rename.
Also tightens the membership lookup to (user_id, company_id)
instead of first-match on user_id so re-running on a user with
multiple companies does the right thing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
import 'dotenv/config';
|
|
import { and, eq } from 'drizzle-orm';
|
|
import { pool } from '../src/lib/server/db/client';
|
|
import { db } from '../src/lib/server/db/client';
|
|
import { users, companies, companyUsers } from '../src/lib/server/db/schema/tenancy';
|
|
import { hashPassword } from '../src/lib/server/auth/password';
|
|
import { normalizeEmail } from '../src/lib/utils/email';
|
|
|
|
function slugify(s: string): string {
|
|
return s
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
}
|
|
|
|
function stripSurroundingQuotes(s: string | undefined): string | undefined {
|
|
if (!s || s.length < 2) return s;
|
|
const first = s[0];
|
|
const last = s[s.length - 1];
|
|
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
|
|
return s.slice(1, -1);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function readArg(flag: string, fallback?: string): string | undefined {
|
|
const i = process.argv.indexOf(flag);
|
|
return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
|
|
}
|
|
|
|
async function main() {
|
|
const email = readArg('--email');
|
|
const password = readArg('--password');
|
|
const name = readArg('--name');
|
|
const companyName = readArg('--company');
|
|
const role = (readArg('--role', 'admin') ?? 'admin') as 'admin' | 'manager' | 'user' | 'viewer';
|
|
|
|
if (!email || !password || !name) {
|
|
console.error('Usage: npm run create-user -- --email <e> --password <p> --name <n> [--company <c>] [--role admin|manager|user|viewer]');
|
|
process.exit(1);
|
|
}
|
|
|
|
const normalized = normalizeEmail(email);
|
|
const hash = await hashPassword(password);
|
|
|
|
const [existing] = await db.select().from(users).where(eq(users.emailNormalized, normalized)).limit(1);
|
|
let userId: string;
|
|
if (existing) {
|
|
console.log(`User ${normalized} already exists; updating password.`);
|
|
await db.update(users).set({ passwordHash: hash, displayName: name }).where(eq(users.id, existing.id));
|
|
userId = existing.id;
|
|
} else {
|
|
const [created] = await db
|
|
.insert(users)
|
|
.values({ email, emailNormalized: normalized, displayName: name, passwordHash: hash })
|
|
.returning({ id: users.id });
|
|
userId = created.id;
|
|
console.log(`Created user ${normalized} (id ${userId}).`);
|
|
}
|
|
|
|
if (companyName) {
|
|
const slug = slugify(companyName);
|
|
// Look up by slug so a previously-corrupted name (e.g. "'B4L'" with literal
|
|
// quotes) doesn't cause a duplicate-slug insert on re-run.
|
|
let [company] = await db.select().from(companies).where(eq(companies.slug, slug)).limit(1);
|
|
if (!company) {
|
|
const [created] = await db
|
|
.insert(companies)
|
|
.values({ name: companyName, slug })
|
|
.returning();
|
|
company = created;
|
|
console.log(`Created company ${companyName} (id ${company.id}).`);
|
|
} else if (company.name !== companyName) {
|
|
await db.update(companies).set({ name: companyName }).where(eq(companies.id, company.id));
|
|
console.log(
|
|
`Renamed company ${JSON.stringify(company.name)} → ${JSON.stringify(companyName)} (id ${company.id}).`
|
|
);
|
|
company = { ...company, name: companyName };
|
|
}
|
|
|
|
const [link] = await db
|
|
.select()
|
|
.from(companyUsers)
|
|
.where(and(eq(companyUsers.userId, userId), eq(companyUsers.companyId, company.id)))
|
|
.limit(1);
|
|
if (!link) {
|
|
await db
|
|
.insert(companyUsers)
|
|
.values({ companyId: company.id, userId, role })
|
|
.onConflictDoNothing();
|
|
console.log(`Linked user to company ${company.name} as ${role}.`);
|
|
} else if (link.role !== role) {
|
|
await db.update(companyUsers).set({ role }).where(eq(companyUsers.id, link.id));
|
|
console.log(`Updated role in ${company.name} to ${role}.`);
|
|
}
|
|
}
|
|
|
|
await pool.end();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|