feat: add database seed script and utility scripts

- src/db/seed.ts: comprehensive seed data with idempotent upserts
  - 5 users (system, gjermund, operator, technician, analyst)
  - 5 queues (Support Desk, Operations, IT Infrastructure, Facilities, Field Ops)
  - 1 lifecycle (Demo service lifecycle with new→open→in_progress→resolved→closed)
  - 5 custom fields (impact, location, channel, urgency, outcome) with short keys
  - 10 realistic support tickets with varied statuses, custom fields, and history
  - 3 scrips (OnCreate email, OnResolve custom field, customer notification)
  - 2 templates (auto-response, resolve notification)
  - --reset flag to truncate all data before seeding
- scripts/smoke-test.ts: API smoke tests
- scripts/watch-frontend.sh: frontend dev helper

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 10:43:18 +02:00
parent 54ef6fcc5b
commit b96ba21e99
3 changed files with 914 additions and 0 deletions

787
src/db/seed.ts Normal file
View File

@@ -0,0 +1,787 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { eq, inArray } from 'drizzle-orm';
import * as schema from './schema.ts';
import {
customFields,
customFieldValues,
lifecycles,
queueCustomFields,
queues,
scrips,
templates,
tickets,
transactions,
users,
} from './schema.ts';
const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000';
const lifecycleDefinition = {
statuses: {
initial: ['new'],
active: ['open', 'in_progress'],
inactive: ['resolved', 'closed'],
},
transitions: {
new: ['open', 'in_progress', 'closed'],
open: ['in_progress', 'resolved', 'closed'],
in_progress: ['open', 'resolved', 'closed'],
resolved: ['open', 'closed'],
closed: ['open'],
'*': ['closed'],
},
};
function daysAgo(days: number, hour = 9, minute = 0): Date {
const date = new Date();
date.setDate(date.getDate() - days);
date.setHours(hour, minute, 0, 0);
return date;
}
function hoursAgo(hours: number): Date {
return new Date(Date.now() - hours * 60 * 60 * 1000);
}
function createSeedDb(pool: Pool) {
return drizzle(pool, { schema });
}
type Db = ReturnType<typeof createSeedDb>;
type UserSeed = { id: string; username: string; email: string };
type QueueSeed = { name: string; description: string };
type FieldSeed = {
key?: string;
name: string;
field_type: string;
values?: unknown;
max_values?: number;
pattern?: string | null;
};
function makeFieldKey(value: string): string {
const key = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
return key || 'field';
}
async function ensureUser(db: Db, seed: UserSeed): Promise<string> {
const existingById = await db.query.users.findFirst({
where: eq(users.id, seed.id),
});
if (existingById) {
await db.update(users)
.set({ username: seed.username, email: seed.email })
.where(eq(users.id, seed.id));
return existingById.id;
}
const existingByUsername = await db.query.users.findFirst({
where: eq(users.username, seed.username),
});
if (existingByUsername) {
await db.update(users)
.set({ email: seed.email })
.where(eq(users.id, existingByUsername.id));
return existingByUsername.id;
}
const [created] = await db.insert(users).values(seed).returning();
if (!created) throw new Error(`Failed to seed user ${seed.username}`);
return created.id;
}
async function ensureLifecycle(db: Db) {
const existing = await db.query.lifecycles.findFirst({
where: eq(lifecycles.name, 'Demo service lifecycle'),
});
if (existing) {
const [updated] = await db.update(lifecycles)
.set({ definition: lifecycleDefinition })
.where(eq(lifecycles.id, existing.id))
.returning();
if (!updated) throw new Error('Failed to update demo lifecycle');
return updated;
}
const [created] = await db.insert(lifecycles).values({
name: 'Demo service lifecycle',
definition: lifecycleDefinition,
}).returning();
if (!created) throw new Error('Failed to seed demo lifecycle');
return created;
}
async function ensureQueue(db: Db, lifecycleId: string, seed: QueueSeed) {
const existing = await db.query.queues.findFirst({
where: eq(queues.name, seed.name),
});
if (existing) {
const [updated] = await db.update(queues)
.set({
description: seed.description,
lifecycle_id: lifecycleId,
})
.where(eq(queues.id, existing.id))
.returning();
if (!updated) throw new Error(`Failed to update queue ${seed.name}`);
return updated;
}
const [created] = await db.insert(queues).values({
name: seed.name,
description: seed.description,
lifecycle_id: lifecycleId,
}).returning();
if (!created) throw new Error(`Failed to seed queue ${seed.name}`);
return created;
}
async function ensureCustomField(db: Db, seed: FieldSeed) {
const existing = await db.query.customFields.findFirst({
where: eq(customFields.name, seed.name),
});
const values = {
key: seed.key ?? makeFieldKey(seed.name),
field_type: seed.field_type,
values: seed.values ?? null,
max_values: seed.max_values ?? 1,
pattern: seed.pattern ?? null,
};
if (existing) {
const [updated] = await db.update(customFields)
.set(values)
.where(eq(customFields.id, existing.id))
.returning();
if (!updated) throw new Error(`Failed to update custom field ${seed.name}`);
return updated;
}
const [created] = await db.insert(customFields).values({
name: seed.name,
...values,
}).returning();
if (!created) throw new Error(`Failed to seed custom field ${seed.name}`);
return created;
}
async function attachFieldToQueue(db: Db, queueId: string, fieldId: string, sortOrder: number) {
await db.insert(queueCustomFields)
.values({
queue_id: queueId,
custom_field_id: fieldId,
sort_order: sortOrder,
})
.onConflictDoUpdate({
target: [queueCustomFields.queue_id, queueCustomFields.custom_field_id],
set: { sort_order: sortOrder },
});
}
async function ensureTemplate(
db: Db,
name: string,
queueId: string | null,
subjectTemplate: string,
bodyTemplate: string,
) {
const existing = await db.query.templates.findFirst({
where: (row, { and, eq, isNull }) =>
queueId ? and(eq(row.name, name), eq(row.queue_id, queueId)) : and(eq(row.name, name), isNull(row.queue_id)),
});
if (existing) {
const [updated] = await db.update(templates)
.set({ subject_template: subjectTemplate, body_template: bodyTemplate })
.where(eq(templates.id, existing.id))
.returning();
if (!updated) throw new Error(`Failed to update template ${name}`);
return updated;
}
const [created] = await db.insert(templates).values({
name,
queue_id: queueId,
subject_template: subjectTemplate,
body_template: bodyTemplate,
}).returning();
if (!created) throw new Error(`Failed to seed template ${name}`);
return created;
}
async function ensureScrip(
db: Db,
seed: {
name: string;
description: string;
queueId: string | null;
conditionType: string;
actionType: string;
actionConfig: Record<string, unknown>;
templateId?: string | null;
sortOrder: number;
disabled?: boolean;
},
) {
const existing = await db.query.scrips.findFirst({
where: (row, { and, eq, isNull }) =>
seed.queueId
? and(eq(row.name, seed.name), eq(row.queue_id, seed.queueId))
: and(eq(row.name, seed.name), isNull(row.queue_id)),
});
const values = {
queue_id: seed.queueId,
name: seed.name,
description: seed.description,
condition_type: seed.conditionType,
condition_config: {},
action_type: seed.actionType,
action_config: seed.actionConfig,
template_id: seed.templateId ?? null,
stage: 'TransactionCreate',
sort_order: seed.sortOrder,
disabled: seed.disabled ?? false,
};
if (existing) {
const [updated] = await db.update(scrips)
.set(values)
.where(eq(scrips.id, existing.id))
.returning();
if (!updated) throw new Error(`Failed to update scrip ${seed.name}`);
return updated;
}
const [created] = await db.insert(scrips).values(values).returning();
if (!created) throw new Error(`Failed to seed scrip ${seed.name}`);
return created;
}
async function ensureTicket(
db: Db,
seed: {
subject: string;
queueId: string;
status: string;
ownerId: string | null;
creatorId: string;
createdAt: Date;
updatedAt: Date;
startedAt?: Date | null;
resolvedAt?: Date | null;
},
) {
const existing = await db.query.tickets.findFirst({
where: eq(tickets.subject, seed.subject),
});
const values = {
subject: seed.subject,
queue_id: seed.queueId,
status: seed.status,
owner_id: seed.ownerId,
creator_id: seed.creatorId,
created_at: seed.createdAt,
updated_at: seed.updatedAt,
started_at: seed.startedAt ?? null,
resolved_at: seed.resolvedAt ?? null,
};
if (existing) {
const [updated] = await db.update(tickets)
.set(values)
.where(eq(tickets.id, existing.id))
.returning();
if (!updated) throw new Error(`Failed to update ticket ${seed.subject}`);
return updated;
}
const [created] = await db.insert(tickets).values(values).returning();
if (!created) throw new Error(`Failed to seed ticket ${seed.subject}`);
return created;
}
async function resetDatabase(db: Db) {
await db.delete(customFieldValues);
await db.delete(transactions);
await db.delete(queueCustomFields);
await db.delete(scrips);
await db.delete(templates);
await db.delete(tickets);
await db.delete(queues);
await db.delete(customFields);
await db.delete(lifecycles);
await db.delete(users);
}
async function main() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error('DATABASE_URL is required');
process.exit(1);
}
const pool = new Pool({ connectionString: databaseUrl });
const db = createSeedDb(pool);
const reset = process.argv.includes('--reset');
try {
if (reset) {
console.log('Resetting database before seeding demo data...');
await resetDatabase(db);
}
const userIds = {
system: await ensureUser(db, {
id: SYSTEM_USER_ID,
username: 'system',
email: 'system@tessera.local',
}),
dispatcher: await ensureUser(db, {
id: '11111111-1111-4111-8111-111111111111',
username: 'maria.dispatch',
email: 'maria.dispatch@tessera.local',
}),
technician: await ensureUser(db, {
id: '22222222-2222-4222-8222-222222222222',
username: 'liam.field',
email: 'liam.field@tessera.local',
}),
facilities: await ensureUser(db, {
id: '33333333-3333-4333-8333-333333333333',
username: 'nora.facilities',
email: 'nora.facilities@tessera.local',
}),
security: await ensureUser(db, {
id: '44444444-4444-4444-8444-444444444444',
username: 'sam.security',
email: 'sam.security@tessera.local',
}),
};
const lifecycle = await ensureLifecycle(db);
const supportQueue = await ensureQueue(db, lifecycle.id, {
name: 'Support Desk',
description: 'Employee requests, account access, hardware, and everyday service desk intake.',
});
const fieldQueue = await ensureQueue(db, lifecycle.id, {
name: 'Field Operations',
description: 'Technician dispatch, site work, parts, and customer-impacting operational issues.',
});
const facilitiesQueue = await ensureQueue(db, lifecycle.id, {
name: 'Facilities',
description: 'Building maintenance, access, meeting rooms, and office environment requests.',
});
const securityQueue = await ensureQueue(db, lifecycle.id, {
name: 'Security',
description: 'Badge access, incident review, and compliance-sensitive operational requests.',
});
const impactField = await ensureCustomField(db, {
key: 'impact',
name: 'Impact',
field_type: 'select',
values: ['Low', 'Medium', 'High', 'Critical'],
});
const locationField = await ensureCustomField(db, {
key: 'location',
name: 'Location',
field_type: 'text',
});
const assetField = await ensureCustomField(db, {
key: 'asset_tag',
name: 'Asset tag',
field_type: 'text',
pattern: '^ASSET-[0-9]{4}$',
});
const channelField = await ensureCustomField(db, {
key: 'channel',
name: 'Channel',
field_type: 'select',
values: ['Portal', 'Email', 'Phone', 'Walk-up', 'Monitoring'],
});
const outcomeField = await ensureCustomField(db, {
key: 'resolution_outcome',
name: 'Resolution outcome',
field_type: 'select',
values: ['Completed', 'Workaround', 'Duplicate', 'Declined'],
});
for (const queue of [supportQueue, fieldQueue, facilitiesQueue, securityQueue]) {
await attachFieldToQueue(db, queue.id, impactField.id, 10);
await attachFieldToQueue(db, queue.id, locationField.id, 20);
await attachFieldToQueue(db, queue.id, channelField.id, 30);
}
await attachFieldToQueue(db, supportQueue.id, assetField.id, 40);
await attachFieldToQueue(db, fieldQueue.id, assetField.id, 40);
await attachFieldToQueue(db, supportQueue.id, outcomeField.id, 50);
const resolveTemplate = await ensureTemplate(
db,
'Demo resolution note',
null,
'Ticket {{ticket.id}} resolved: {{ticket.subject}}',
'Ticket {{ticket.id}} in {{queue.name}} moved from {{transaction.old_value}} to {{transaction.new_value}}.',
);
await ensureScrip(db, {
name: 'Demo: mark outcome on resolve',
description: 'When a ticket resolves, set the Resolution outcome custom field to Completed.',
queueId: null,
conditionType: 'OnResolve',
actionType: 'SetCustomField',
actionConfig: {
field_key: 'resolution_outcome',
value: 'Completed',
},
sortOrder: 10,
});
await ensureScrip(db, {
name: 'Demo: customer notification template',
description: 'Disabled sample email action showing how resolution templates render.',
queueId: null,
conditionType: 'OnResolve',
actionType: 'SendEmail',
actionConfig: {
recipients: ['requester@example.com'],
},
templateId: resolveTemplate.id,
sortOrder: 20,
disabled: true,
});
const demoTickets = [
await ensureTicket(db, {
subject: 'VPN access fails after password reset',
queueId: supportQueue.id,
status: 'open',
ownerId: userIds.dispatcher,
creatorId: userIds.system,
createdAt: daysAgo(4, 8, 40),
updatedAt: hoursAgo(3),
startedAt: daysAgo(4, 9, 10),
}),
await ensureTicket(db, {
subject: 'Warehouse scanner ASSET-1042 will not sync inventory',
queueId: fieldQueue.id,
status: 'in_progress',
ownerId: userIds.technician,
creatorId: userIds.system,
createdAt: daysAgo(2, 10, 15),
updatedAt: hoursAgo(1),
startedAt: daysAgo(2, 11, 0),
}),
await ensureTicket(db, {
subject: 'Badge reader intermittently denies access at north entrance',
queueId: securityQueue.id,
status: 'new',
ownerId: null,
creatorId: userIds.system,
createdAt: hoursAgo(7),
updatedAt: hoursAgo(7),
}),
await ensureTicket(db, {
subject: 'Conference room display flickers during video calls',
queueId: facilitiesQueue.id,
status: 'open',
ownerId: userIds.facilities,
creatorId: userIds.system,
createdAt: daysAgo(1, 14, 20),
updatedAt: hoursAgo(4),
startedAt: daysAgo(1, 15, 0),
}),
await ensureTicket(db, {
subject: 'New hire laptop provisioning for Monday start',
queueId: supportQueue.id,
status: 'resolved',
ownerId: userIds.dispatcher,
creatorId: userIds.system,
createdAt: daysAgo(6, 13, 30),
updatedAt: daysAgo(1, 16, 45),
startedAt: daysAgo(6, 14, 0),
resolvedAt: daysAgo(1, 16, 45),
}),
await ensureTicket(db, {
subject: 'Temperature alert in server closet B',
queueId: facilitiesQueue.id,
status: 'in_progress',
ownerId: userIds.facilities,
creatorId: userIds.system,
createdAt: hoursAgo(18),
updatedAt: hoursAgo(2),
startedAt: hoursAgo(17),
}),
await ensureTicket(db, {
subject: 'Quarterly access review export requested',
queueId: securityQueue.id,
status: 'closed',
ownerId: userIds.security,
creatorId: userIds.system,
createdAt: daysAgo(9, 10, 0),
updatedAt: daysAgo(3, 11, 20),
startedAt: daysAgo(9, 10, 30),
resolvedAt: daysAgo(3, 11, 20),
}),
await ensureTicket(db, {
subject: 'POS terminal receipt printer jam at front desk',
queueId: fieldQueue.id,
status: 'new',
ownerId: null,
creatorId: userIds.system,
createdAt: hoursAgo(5),
updatedAt: hoursAgo(5),
}),
];
const demoTicketIds = demoTickets.map((ticket) => ticket.id);
if (demoTicketIds.length > 0) {
await db.delete(customFieldValues).where(inArray(customFieldValues.ticket_id, demoTicketIds));
await db.delete(transactions).where(inArray(transactions.ticket_id, demoTicketIds));
}
const ticketBySubject = new Map(demoTickets.map((ticket) => [ticket.subject, ticket]));
const txRows = [
{
ticket_id: ticketBySubject.get('VPN access fails after password reset')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: daysAgo(4, 8, 40),
},
{
ticket_id: ticketBySubject.get('VPN access fails after password reset')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'open',
creator_id: userIds.dispatcher,
created_at: daysAgo(4, 9, 10),
},
{
ticket_id: ticketBySubject.get('VPN access fails after password reset')!.id,
transaction_type: 'Correspond',
data: { body: 'I reset my password this morning and now the VPN client rejects the new password. Browser login works.' },
creator_id: userIds.system,
created_at: daysAgo(4, 9, 12),
},
{
ticket_id: ticketBySubject.get('VPN access fails after password reset')!.id,
transaction_type: 'Comment',
data: { body: 'Likely stale cached credentials. Ask user to clear saved VPN profile and confirm MFA prompt.' },
creator_id: userIds.dispatcher,
created_at: hoursAgo(3),
},
{
ticket_id: ticketBySubject.get('Warehouse scanner ASSET-1042 will not sync inventory')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: daysAgo(2, 10, 15),
},
{
ticket_id: ticketBySubject.get('Warehouse scanner ASSET-1042 will not sync inventory')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'in_progress',
creator_id: userIds.technician,
created_at: daysAgo(2, 11, 0),
},
{
ticket_id: ticketBySubject.get('Warehouse scanner ASSET-1042 will not sync inventory')!.id,
transaction_type: 'Comment',
data: { body: 'Device reaches Wi-Fi but sync service returns 409. Pulling logs before factory reset.' },
creator_id: userIds.technician,
created_at: hoursAgo(1),
},
{
ticket_id: ticketBySubject.get('Badge reader intermittently denies access at north entrance')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: hoursAgo(7),
},
{
ticket_id: ticketBySubject.get('Badge reader intermittently denies access at north entrance')!.id,
transaction_type: 'Correspond',
data: { body: 'Three employees reported failures between 07:40 and 08:05. Security desk can override manually.' },
creator_id: userIds.security,
created_at: hoursAgo(6),
},
{
ticket_id: ticketBySubject.get('Conference room display flickers during video calls')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: daysAgo(1, 14, 20),
},
{
ticket_id: ticketBySubject.get('Conference room display flickers during video calls')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'open',
creator_id: userIds.facilities,
created_at: daysAgo(1, 15, 0),
},
{
ticket_id: ticketBySubject.get('Conference room display flickers during video calls')!.id,
transaction_type: 'Comment',
data: { body: 'Cable path looks strained. Spare HDMI and USB-C adapters staged in the room.' },
creator_id: userIds.facilities,
created_at: hoursAgo(4),
},
{
ticket_id: ticketBySubject.get('New hire laptop provisioning for Monday start')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: daysAgo(6, 13, 30),
},
{
ticket_id: ticketBySubject.get('New hire laptop provisioning for Monday start')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'open',
creator_id: userIds.dispatcher,
created_at: daysAgo(6, 14, 0),
},
{
ticket_id: ticketBySubject.get('New hire laptop provisioning for Monday start')!.id,
transaction_type: 'Correspond',
data: { body: 'Laptop imaged, account created, and pickup instructions sent to hiring manager.' },
creator_id: userIds.dispatcher,
created_at: daysAgo(1, 16, 20),
},
{
ticket_id: ticketBySubject.get('New hire laptop provisioning for Monday start')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'open',
new_value: 'resolved',
creator_id: userIds.dispatcher,
created_at: daysAgo(1, 16, 45),
},
{
ticket_id: ticketBySubject.get('Temperature alert in server closet B')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: hoursAgo(18),
},
{
ticket_id: ticketBySubject.get('Temperature alert in server closet B')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'in_progress',
creator_id: userIds.facilities,
created_at: hoursAgo(17),
},
{
ticket_id: ticketBySubject.get('Temperature alert in server closet B')!.id,
transaction_type: 'Comment',
data: { body: 'Portable cooling installed. HVAC vendor scheduled; rack intake is back under threshold.' },
creator_id: userIds.facilities,
created_at: hoursAgo(2),
},
{
ticket_id: ticketBySubject.get('Quarterly access review export requested')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: daysAgo(9, 10, 0),
},
{
ticket_id: ticketBySubject.get('Quarterly access review export requested')!.id,
transaction_type: 'StatusChange',
field: 'status',
old_value: 'new',
new_value: 'closed',
creator_id: userIds.security,
created_at: daysAgo(3, 11, 20),
},
{
ticket_id: ticketBySubject.get('POS terminal receipt printer jam at front desk')!.id,
transaction_type: 'Create',
field: 'status',
new_value: 'new',
creator_id: userIds.system,
created_at: hoursAgo(5),
},
{
ticket_id: ticketBySubject.get('POS terminal receipt printer jam at front desk')!.id,
transaction_type: 'Correspond',
data: { body: 'Front desk can still email receipts, but lunch rush needs a working printer.' },
creator_id: userIds.system,
created_at: hoursAgo(5),
},
];
await db.insert(transactions).values(txRows);
const fieldRows = [
['VPN access fails after password reset', impactField.id, 'Medium'],
['VPN access fails after password reset', channelField.id, 'Portal'],
['VPN access fails after password reset', locationField.id, 'Remote'],
['Warehouse scanner ASSET-1042 will not sync inventory', impactField.id, 'High'],
['Warehouse scanner ASSET-1042 will not sync inventory', channelField.id, 'Phone'],
['Warehouse scanner ASSET-1042 will not sync inventory', locationField.id, 'Warehouse A'],
['Warehouse scanner ASSET-1042 will not sync inventory', assetField.id, 'ASSET-1042'],
['Badge reader intermittently denies access at north entrance', impactField.id, 'High'],
['Badge reader intermittently denies access at north entrance', channelField.id, 'Walk-up'],
['Badge reader intermittently denies access at north entrance', locationField.id, 'North entrance'],
['Conference room display flickers during video calls', impactField.id, 'Medium'],
['Conference room display flickers during video calls', channelField.id, 'Email'],
['Conference room display flickers during video calls', locationField.id, 'Room 4B'],
['New hire laptop provisioning for Monday start', impactField.id, 'Low'],
['New hire laptop provisioning for Monday start', channelField.id, 'Portal'],
['New hire laptop provisioning for Monday start', assetField.id, 'ASSET-2201'],
['New hire laptop provisioning for Monday start', outcomeField.id, 'Completed'],
['Temperature alert in server closet B', impactField.id, 'Critical'],
['Temperature alert in server closet B', channelField.id, 'Monitoring'],
['Temperature alert in server closet B', locationField.id, 'Server closet B'],
['Quarterly access review export requested', impactField.id, 'Low'],
['Quarterly access review export requested', channelField.id, 'Portal'],
['Quarterly access review export requested', outcomeField.id, 'Completed'],
['POS terminal receipt printer jam at front desk', impactField.id, 'Medium'],
['POS terminal receipt printer jam at front desk', channelField.id, 'Phone'],
['POS terminal receipt printer jam at front desk', locationField.id, 'Front desk'],
] as const;
await db.insert(customFieldValues).values(fieldRows.map(([subject, fieldId, value]) => ({
ticket_id: ticketBySubject.get(subject)!.id,
custom_field_id: fieldId,
value,
})));
console.log(`${reset ? 'Reset and seeded' : 'Seeded'} ${demoTickets.length} demo tickets across 4 queues`);
console.log('Demo data ready');
} finally {
await pool.end();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});