404 lines
10 KiB
TypeScript
404 lines
10 KiB
TypeScript
import {
|
|
AppEnv,
|
|
ErrCode,
|
|
type EventInsert,
|
|
organizations,
|
|
RecaseError,
|
|
} from "@autumn/shared";
|
|
import { type DrizzleCli, initDrizzle } from "@server/db/initDrizzle.js";
|
|
import { EventService } from "@server/internal/api/events/EventService.js";
|
|
import { loadLocalEnv } from "@server/utils/envUtils.js";
|
|
import { generateId } from "@server/utils/genUtils.js";
|
|
import chalk from "chalk";
|
|
|
|
// Load environment variables from server/.env
|
|
loadLocalEnv();
|
|
|
|
// ============================================================================
|
|
// CONFIGURATION - Edit these values to customize seed behavior
|
|
// ============================================================================
|
|
|
|
const CONFIG = {
|
|
// Default number of events to generate
|
|
defaultEventCount: 100,
|
|
|
|
// Time range configuration (in days)
|
|
// Events will be randomly distributed within this range from now
|
|
timeRangeDays: 30,
|
|
|
|
// Default environment
|
|
defaultEnv: AppEnv.Sandbox,
|
|
|
|
// Optional default properties to include in events
|
|
defaultProperties: {
|
|
source: "seed_script",
|
|
version: "1.0",
|
|
},
|
|
} as const;
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
interface CliArgs {
|
|
customer_id: string;
|
|
org_slug: string;
|
|
count?: number;
|
|
env?: AppEnv;
|
|
feature_ids: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI ARGUMENT PARSING
|
|
// ============================================================================
|
|
|
|
function parseArgs(): CliArgs {
|
|
const args = process.argv.slice(2);
|
|
const parsed: Partial<CliArgs> = {};
|
|
|
|
for (const arg of args) {
|
|
if (arg.startsWith("--")) {
|
|
const [key, value] = arg.slice(2).split("=");
|
|
if (key === "customer_id") {
|
|
parsed.customer_id = value;
|
|
} else if (key === "org_slug") {
|
|
parsed.org_slug = value;
|
|
} else if (key === "count") {
|
|
parsed.count = Number.parseInt(value, 10);
|
|
} else if (key === "env") {
|
|
parsed.env = value as AppEnv;
|
|
} else if (key === "feature_ids") {
|
|
parsed.feature_ids = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!parsed.customer_id) {
|
|
console.error(chalk.red("❌ Error: --customer_id is required"));
|
|
console.log(
|
|
chalk.yellow(
|
|
"\nUsage: bun run scripts/seed/seedEvents.ts --customer_id=<id> --org_slug=<slug> --feature_ids=<id1,id2,id3> [--count=<number>] [--env=<sandbox|live>]",
|
|
),
|
|
);
|
|
console.log(chalk.gray("\nExample:"));
|
|
console.log(
|
|
chalk.gray(
|
|
" bun run scripts/seed/seedEvents.ts --customer_id=cus_123 --org_slug=my-org --feature_ids=api_call,page_view --count=50 --env=sandbox",
|
|
),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!parsed.org_slug) {
|
|
console.error(chalk.red("❌ Error: --org_slug is required"));
|
|
console.log(
|
|
chalk.yellow(
|
|
"\nUsage: bun run scripts/seed/seedEvents.ts --customer_id=<id> --org_slug=<slug> --feature_ids=<id1,id2,id3> [--count=<number>] [--env=<sandbox|live>]",
|
|
),
|
|
);
|
|
console.log(chalk.gray("\nExample:"));
|
|
console.log(
|
|
chalk.gray(
|
|
" bun run scripts/seed/seedEvents.ts --customer_id=cus_123 --org_slug=my-org --feature_ids=api_call,page_view --count=50 --env=sandbox",
|
|
),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!parsed.feature_ids) {
|
|
console.error(chalk.red("❌ Error: --feature_ids is required"));
|
|
console.log(
|
|
chalk.yellow(
|
|
"\nUsage: bun run scripts/seed/seedEvents.ts --customer_id=<id> --org_slug=<slug> --feature_ids=<id1,id2,id3> [--count=<number>] [--env=<sandbox|live>]",
|
|
),
|
|
);
|
|
console.log(chalk.gray("\nExample:"));
|
|
console.log(
|
|
chalk.gray(
|
|
" bun run scripts/seed/seedEvents.ts --customer_id=cus_123 --org_slug=my-org --feature_ids=api_call,page_view --count=50 --env=sandbox",
|
|
),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
return parsed as CliArgs;
|
|
}
|
|
|
|
// ============================================================================
|
|
// VALIDATION
|
|
// ============================================================================
|
|
|
|
async function validateOrg({
|
|
db,
|
|
orgSlug,
|
|
}: {
|
|
db: DrizzleCli;
|
|
orgSlug: string;
|
|
}) {
|
|
const org = await db.query.organizations.findFirst({
|
|
where: (orgs, { eq }) => eq(orgs.slug, orgSlug),
|
|
});
|
|
|
|
if (!org) {
|
|
throw new RecaseError({
|
|
message: `Organization with slug '${orgSlug}' not found`,
|
|
code: ErrCode.OrgNotFound,
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
return org;
|
|
}
|
|
|
|
async function validateCustomer({
|
|
db,
|
|
customerId,
|
|
orgId,
|
|
env,
|
|
}: {
|
|
db: DrizzleCli;
|
|
customerId: string;
|
|
orgId: string;
|
|
env: AppEnv;
|
|
}) {
|
|
const customer = await db.query.customers.findFirst({
|
|
where: (customers, { eq, or, and }) => {
|
|
return and(
|
|
or(eq(customers.id, customerId), eq(customers.internal_id, customerId)),
|
|
eq(customers.org_id, orgId),
|
|
eq(customers.env, env),
|
|
);
|
|
},
|
|
});
|
|
|
|
if (!customer) {
|
|
throw new RecaseError({
|
|
message: `Customer '${customerId}' not found in org '${orgId}' with env '${env}'`,
|
|
code: ErrCode.CustomerNotFound,
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
return customer;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EVENT GENERATION
|
|
// ============================================================================
|
|
|
|
function generateRandomTimestamp({ daysBack }: { daysBack: number }): Date {
|
|
const now = Date.now();
|
|
const startTime = now - daysBack * 24 * 60 * 60 * 1000;
|
|
const randomTime = startTime + Math.random() * (now - startTime);
|
|
return new Date(randomTime);
|
|
}
|
|
|
|
function generateEvents({
|
|
count,
|
|
customer,
|
|
org,
|
|
env,
|
|
featureIds,
|
|
}: {
|
|
count: number;
|
|
customer: Awaited<ReturnType<typeof validateCustomer>>;
|
|
org: Awaited<ReturnType<typeof validateOrg>>;
|
|
env: AppEnv;
|
|
featureIds: string[];
|
|
}): EventInsert[] {
|
|
const events: EventInsert[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const eventName = featureIds[Math.floor(Math.random() * featureIds.length)];
|
|
const timestamp = generateRandomTimestamp({
|
|
daysBack: CONFIG.timeRangeDays,
|
|
});
|
|
|
|
const event: EventInsert = {
|
|
id: generateId("evt"),
|
|
org_id: org.id,
|
|
org_slug: org.slug,
|
|
internal_customer_id: customer.internal_id,
|
|
customer_id: customer.id || "",
|
|
env,
|
|
event_name: eventName,
|
|
timestamp,
|
|
created_at: Date.now(),
|
|
value: null,
|
|
set_usage: false,
|
|
entity_id: null,
|
|
internal_entity_id: null,
|
|
idempotency_key: null,
|
|
properties: {
|
|
...CONFIG.defaultProperties,
|
|
event_index: i + 1,
|
|
random_value: Math.floor(Math.random() * 1000),
|
|
user: {
|
|
id: Math.floor(Math.random() * 10) + 1,
|
|
},
|
|
},
|
|
};
|
|
|
|
events.push(event);
|
|
}
|
|
|
|
// Sort events by timestamp for more realistic insertion
|
|
events.sort(
|
|
(a, b) => (a.timestamp?.getTime() || 0) - (b.timestamp?.getTime() || 0),
|
|
);
|
|
|
|
return events;
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN EXECUTION
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
console.log(
|
|
chalk.magentaBright(
|
|
"\n================ Event Seeding Script ================\n",
|
|
),
|
|
);
|
|
|
|
// Parse CLI arguments
|
|
const args = parseArgs();
|
|
const eventCount = args.count || CONFIG.defaultEventCount;
|
|
const env = args.env || CONFIG.defaultEnv;
|
|
const featureIds = args.feature_ids.split(",").map((id) => id.trim());
|
|
|
|
console.log(chalk.cyan("Configuration:"));
|
|
console.log(chalk.gray(` Customer ID: ${args.customer_id}`));
|
|
console.log(chalk.gray(` Organization Slug: ${args.org_slug}`));
|
|
console.log(chalk.gray(` Event Count: ${eventCount}`));
|
|
console.log(chalk.gray(` Environment: ${env}`));
|
|
console.log(chalk.gray(` Feature IDs: ${featureIds.join(", ")}`));
|
|
console.log();
|
|
|
|
// Initialize database connection
|
|
const { db, client } = initDrizzle();
|
|
|
|
try {
|
|
// Validate organization exists
|
|
console.log(chalk.cyan("Validating organization..."));
|
|
const org = await validateOrg({
|
|
db,
|
|
orgSlug: args.org_slug,
|
|
});
|
|
|
|
console.log(
|
|
chalk.green(`✅ Organization found: ${org.slug} (${org.name})`),
|
|
);
|
|
console.log();
|
|
|
|
// Validate customer exists
|
|
console.log(chalk.cyan("Validating customer..."));
|
|
const customer = await validateCustomer({
|
|
db,
|
|
customerId: args.customer_id,
|
|
orgId: org.id,
|
|
env,
|
|
});
|
|
|
|
console.log(
|
|
chalk.green(
|
|
`✅ Customer found: ${customer.id} (${customer.name || "No name"})`,
|
|
),
|
|
);
|
|
console.log();
|
|
|
|
// Generate events
|
|
console.log(chalk.cyan(`Generating ${eventCount} events...`));
|
|
const events = generateEvents({
|
|
count: eventCount,
|
|
customer,
|
|
org,
|
|
env,
|
|
featureIds,
|
|
});
|
|
|
|
console.log(chalk.green(`✅ Generated ${events.length} events`));
|
|
console.log(
|
|
chalk.gray(
|
|
` Time range: ${events[0].timestamp?.toISOString()} to ${events[events.length - 1].timestamp?.toISOString()}`,
|
|
),
|
|
);
|
|
console.log(
|
|
chalk.gray(
|
|
` Event types: ${Array.from(new Set(events.map((e) => e.event_name))).join(", ")}`,
|
|
),
|
|
);
|
|
console.log();
|
|
|
|
// Insert events
|
|
console.log(chalk.cyan("Inserting events into database..."));
|
|
let successCount = 0;
|
|
let errorCount = 0;
|
|
|
|
// Insert in batches to avoid overwhelming the database
|
|
const batchSize = 50;
|
|
for (let i = 0; i < events.length; i += batchSize) {
|
|
const batch = events.slice(i, i + batchSize);
|
|
const batchNum = Math.floor(i / batchSize) + 1;
|
|
const totalBatches = Math.ceil(events.length / batchSize);
|
|
|
|
process.stdout.write(
|
|
chalk.gray(` Batch ${batchNum}/${totalBatches}... `),
|
|
);
|
|
|
|
for (const event of batch) {
|
|
try {
|
|
await EventService.insert({ db, event });
|
|
successCount++;
|
|
} catch (error) {
|
|
errorCount++;
|
|
if (errorCount <= 5) {
|
|
// Only show first 5 errors to avoid spam
|
|
console.log(
|
|
chalk.yellow(
|
|
`\n ⚠️ Error inserting event ${event.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green("✓"));
|
|
}
|
|
|
|
console.log();
|
|
console.log(chalk.green(`✅ Successfully inserted ${successCount} events`));
|
|
|
|
if (errorCount > 0) {
|
|
console.log(chalk.yellow(`⚠️ ${errorCount} events failed to insert`));
|
|
if (errorCount > 5) {
|
|
console.log(
|
|
chalk.gray(` (Only first 5 errors shown to reduce spam)`),
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.magentaBright(
|
|
"\n================ Seeding Complete ================\n",
|
|
),
|
|
);
|
|
} catch (error) {
|
|
console.error(chalk.red("\n❌ Error during seeding:"));
|
|
if (error instanceof RecaseError) {
|
|
console.error(chalk.red(` ${error.message}`));
|
|
} else if (error instanceof Error) {
|
|
console.error(chalk.red(` ${error.message}`));
|
|
console.error(chalk.gray(error.stack));
|
|
} else {
|
|
console.error(chalk.red(` ${String(error)}`));
|
|
}
|
|
process.exit(1);
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
}
|
|
|
|
// Run the script
|
|
main();
|