rediseed 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +463 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +440 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +366 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +321 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/loaders/file.ts","../src/engine/client.ts","../src/utils/template.ts","../src/engine/seeder.ts","../src/engine/inspector.ts","../src/utils/k6.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport picocolors from \"picocolors\";\nimport { loadAndParseFile } from \"./loaders/file.js\";\nimport { createRedisClient } from \"./engine/client.js\";\nimport { executeSeed } from \"./engine/seeder.js\";\nimport { inspectKey } from \"./engine/inspector.js\";\nimport { generateK6Snippet } from \"./utils/k6.js\";\nimport { RedisStructure } from \"./types.js\";\n\nasync function main() {\n const program = new Command();\n\n program\n .name(\"rediseed\")\n .description(\n \"Blazing-fast Redis data seeder & loader from CSV, JSON, and line streams for k6 load testing & staging fixtures.\"\n )\n .version(\"1.0.0\", \"-v, --version\", \"Output current version\");\n\n // Global Redis Options\n program\n .option(\"-u, --url <url>\", \"Redis connection URL (e.g. redis://localhost:6379 or rediss:// for TLS)\")\n .option(\"-h, --host <host>\", \"Redis host\", \"127.0.0.1\")\n .option(\"-p, --port <port>\", \"Redis port\", \"6379\")\n .option(\"-a, --password <password>\", \"Redis auth password\")\n .option(\"-n, --db <db>\", \"Redis database index (0-15)\", \"0\")\n .option(\"--tls\", \"Enable TLS connection\", false);\n\n // 1. Seed Command\n program\n .command(\"seed <file>\")\n .description(\"Load and seed data from CSV, JSON, or TXT file into Redis\")\n .option(\"-s, --structure <type>\", \"Redis structure: set, list, hash, string, zset\", \"set\")\n .option(\"-k, --key <name>\", \"Destination Redis key name or template (e.g. 'k6:user_ids' or 'user:{{id}}')\", \"seed:data\")\n .option(\"-c, --column <col>\", \"Extract specific column/property (for set, list, string)\")\n .option(\"--value <template>\", \"Value template for string/hash (e.g. '{{email}}:{{token}}')\")\n .option(\"--score-column <col>\", \"Column for zset score (defaults to timestamp)\")\n .option(\"-b, --batch-size <size>\", \"Pipeline batch size\", \"1000\")\n .option(\"--ttl <seconds>\", \"TTL expiration in seconds for keys\")\n .option(\"--flush\", \"Delete destination key before inserting\", false)\n .option(\"--dry-run\", \"Preview parsing and first 5 rows without writing to Redis\", false)\n .action(async (file, options) => {\n try {\n const globalOpts = program.opts();\n console.log(picocolors.bold(`\\nš± rediseed ā Loading data from: ${picocolors.cyan(file)}`));\n\n const rows = loadAndParseFile(file);\n console.log(` ⢠Parsed ${picocolors.green(rows.length.toLocaleString())} records.`);\n\n if (options.dryRun) {\n console.log(picocolors.yellow(\"\\n[DRY RUN PREVIEW - First 5 items]\"));\n for (const row of rows.slice(0, 5)) {\n console.log(\" \", JSON.stringify(row));\n }\n console.log(picocolors.dim(`\\nWould insert ${rows.length} items into Redis structure '${options.structure}' with key '${options.key}'.\\n`));\n return;\n }\n\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const result = await executeSeed(redis, rows, {\n structure: options.structure as RedisStructure,\n keyName: options.key,\n column: options.column,\n valueTemplate: options.value,\n scoreColumn: options.scoreColumn,\n batchSize: parseInt(options.batchSize, 10),\n flushFirst: options.flush,\n ttl: options.ttl ? parseInt(options.ttl, 10) : undefined,\n });\n\n await redis.quit();\n\n console.log(picocolors.green(`\\nā Seeding completed successfully!`));\n console.log(` ⢠Structure: ${picocolors.cyan(result.structure.toUpperCase())}`);\n console.log(` ⢠Target Key: ${picocolors.cyan(options.key)}`);\n console.log(` ⢠Total Items: ${picocolors.bold(result.totalInserted.toLocaleString())}`);\n console.log(` ⢠Speed: ${picocolors.bold(result.opsPerSec.toLocaleString())} ops/sec (${result.elapsedMs}ms)\\n`);\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 2. Set Shortcut Command (seed directly into a Redis Set for k6 SPOP)\n program\n .command(\"set <keyName> <file>\")\n .description(\"Shortcut to load a file column directly into a Redis Set (ideal for k6 SPOP unique IDs)\")\n .option(\"-c, --column <col>\", \"Extract specific column name from CSV/JSON\")\n .option(\"--flush\", \"Delete set before inserting\", false)\n .action(async (keyName, file, options) => {\n try {\n const globalOpts = program.opts();\n const rows = loadAndParseFile(file);\n\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const result = await executeSeed(redis, rows, {\n structure: \"set\",\n keyName,\n column: options.column,\n flushFirst: options.flush,\n });\n\n await redis.quit();\n\n console.log(picocolors.green(`\\nā Successfully loaded ${result.totalInserted.toLocaleString()} unique items into Redis Set: ${picocolors.cyan(keyName)} (${result.elapsedMs}ms)`));\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 3. Inspect Command\n program\n .command(\"inspect <key>\")\n .description(\"Inspect item count, data type, memory usage, and samples from a Redis key\")\n .option(\"-s, --samples <count>\", \"Number of sample items to preview\", \"5\")\n .action(async (key, options) => {\n try {\n const globalOpts = program.opts();\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const info = await inspectKey(redis, key, parseInt(options.samples, 10));\n await redis.quit();\n\n console.log(`\\n${picocolors.bold(\"š Redis Key Inspector\")}`);\n console.log(` ⢠Key: ${picocolors.cyan(info.key)}`);\n console.log(` ⢠Type: ${picocolors.green(info.type.toUpperCase())}`);\n console.log(` ⢠Total Items: ${picocolors.bold(info.count.toLocaleString())}`);\n console.log(` ⢠TTL: ${info.ttl === -1 ? \"No Expiration\" : `${info.ttl}s`}`);\n if (info.memoryBytes) {\n console.log(` ⢠Memory Usage: ${(info.memoryBytes / 1024).toFixed(2)} KB`);\n }\n console.log(`\\n${picocolors.bold(\"Sample Items:\")}`);\n for (const sample of info.samples) {\n console.log(` ⢠${sample}`);\n }\n console.log(\"\");\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 4. Generate k6 Snippet\n program\n .command(\"k6 <key>\")\n .description(\"Generate ready-to-use k6 load test script snippet consuming this Redis key\")\n .option(\"-s, --structure <type>\", \"Redis structure (set, list, string)\", \"set\")\n .action((key, options) => {\n const snippet = generateK6Snippet(key, options.structure as RedisStructure);\n console.log(`\\n${snippet}\\n`);\n });\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","import fs from \"fs\";\nimport { parse } from \"csv-parse/sync\";\nimport { ParsedRow } from \"../types.js\";\n\n/**\n * Parse a CSV string or file content into array of row objects.\n */\nexport function parseCSV(content: string, delimiter: string = \",\"): ParsedRow[] {\n const records = parse(content, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n delimiter,\n cast: false,\n });\n return records;\n}\n\n/**\n * Parse a JSON array, single JSON object, or JSONL string into array of row objects.\n */\nexport function parseJSONOrJSONL(content: string): ParsedRow[] {\n const trimmed = content.trim();\n if (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")) {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed)) throw new Error(\"JSON root must be an array of objects.\");\n return parsed;\n }\n\n // Check if it is line-delimited JSON (JSONL)\n const lines = trimmed.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\n const rows: ParsedRow[] = [];\n for (const line of lines) {\n if (line.startsWith(\"{\") && line.endsWith(\"}\")) {\n rows.push(JSON.parse(line));\n } else {\n // Plain text single column line\n rows.push({ value: line });\n }\n }\n\n return rows;\n}\n\n/**\n * Parse plain text lines into { value: line } rows.\n */\nexport function parseLines(content: string): ParsedRow[] {\n return content\n .split(/\\r?\\n/)\n .map(l => l.trim())\n .filter(Boolean)\n .map(l => ({ value: l }));\n}\n\n/**\n * Auto-detect file format from extension and contents.\n */\nexport function loadAndParseFile(filePath: string): ParsedRow[] {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n\n const content = fs.readFileSync(filePath, \"utf8\");\n const ext = filePath.split(\".\").pop()?.toLowerCase();\n\n if (ext === \"csv\") {\n return parseCSV(content);\n } else if (ext === \"tsv\") {\n return parseCSV(content, \"\\t\");\n } else if (ext === \"json\" || ext === \"jsonl\" || ext === \"ndjson\") {\n return parseJSONOrJSONL(content);\n } else if (ext === \"txt\") {\n return parseLines(content);\n }\n\n // Fallback try JSON then CSV then lines\n try {\n return parseJSONOrJSONL(content);\n } catch {\n try {\n return parseCSV(content);\n } catch {\n return parseLines(content);\n }\n }\n}\n","import { Redis, RedisOptions } from \"ioredis\";\nimport { RedisConnectionOptions } from \"../types.js\";\n\n/**\n * Create a reusable ioredis client instance.\n */\nexport function createRedisClient(options: RedisConnectionOptions = {}): Redis {\n const url = options.url || process.env.REDIS_URL || process.env.REDIS_URI;\n\n if (url) {\n return new Redis(url, {\n lazyConnect: false,\n maxRetriesPerRequest: 3,\n enableReadyCheck: true,\n });\n }\n\n const redisConfig: RedisOptions = {\n host: options.host || process.env.REDIS_HOST || \"127.0.0.1\",\n port: options.port || (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379),\n password: options.password || process.env.REDIS_PASSWORD || undefined,\n db: options.db !== undefined ? options.db : (process.env.REDIS_DB ? parseInt(process.env.REDIS_DB, 10) : 0),\n maxRetriesPerRequest: 3,\n lazyConnect: false,\n };\n\n if (options.tls) {\n redisConfig.tls = {};\n }\n\n return new Redis(redisConfig);\n}\n","import { ParsedRow } from \"../types.js\";\n\n/**\n * Render template string replacing `{{variable}}` with values from row.\n */\nexport function renderTemplate(template: string, row: ParsedRow): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_-]+)\\s*\\}\\}/g, (_, key) => {\n return row[key] !== undefined ? String(row[key]) : \"\";\n });\n}\n\n/**\n * Extract item value from row based on column or template.\n */\nexport function extractRowValue(row: ParsedRow, column?: string, valueTemplate?: string): string {\n if (valueTemplate) {\n return renderTemplate(valueTemplate, row);\n }\n\n if (column && row[column] !== undefined) {\n return String(row[column]);\n }\n\n // If there's only 1 property in the row, use it\n const keys = Object.keys(row);\n if (keys.length === 1) {\n return String(row[keys[0]]);\n }\n\n // Otherwise return entire row as JSON string\n return JSON.stringify(row);\n}\n","import { Redis } from \"ioredis\";\nimport { SeedOptions, SeedResult, ParsedRow } from \"../types.js\";\nimport { renderTemplate, extractRowValue } from \"../utils/template.js\";\n\n/**\n * Execute high-speed batched seeding into Redis using pipelines.\n */\nexport async function executeSeed(\n redis: Redis,\n rows: ParsedRow[],\n options: SeedOptions\n): Promise<SeedResult> {\n const startTime = Date.now();\n const batchSize = options.batchSize || 1000;\n let totalInserted = 0;\n const uniqueKeys = new Set<string>();\n\n // If flushFirst is enabled and static keyName is provided, delete it first\n if (options.flushFirst && options.keyName && !options.keyName.includes(\"{{\")) {\n await redis.del(options.keyName);\n }\n\n // Process in batches using pipeline\n for (let i = 0; i < rows.length; i += batchSize) {\n const batch = rows.slice(i, i + batchSize);\n const pipeline = redis.pipeline();\n\n for (const row of batch) {\n const targetKey = options.keyName\n ? renderTemplate(options.keyName, row)\n : \"seed:data\";\n\n uniqueKeys.add(targetKey);\n const val = extractRowValue(row, options.column, options.valueTemplate);\n\n switch (options.structure) {\n case \"set\":\n pipeline.sadd(targetKey, val);\n break;\n\n case \"list\":\n pipeline.rpush(targetKey, val);\n break;\n\n case \"string\":\n if (options.ttl) {\n pipeline.set(targetKey, val, \"EX\", options.ttl);\n } else {\n pipeline.set(targetKey, val);\n }\n break;\n\n case \"hash\":\n // If row has multiple columns, store full hash fields\n const hashFields: Record<string, string> = {};\n for (const [k, v] of Object.entries(row)) {\n hashFields[k] = typeof v === \"object\" ? JSON.stringify(v) : String(v);\n }\n pipeline.hset(targetKey, hashFields);\n if (options.ttl) {\n pipeline.expire(targetKey, options.ttl);\n }\n break;\n\n case \"zset\":\n const score = options.scoreColumn && row[options.scoreColumn] !== undefined\n ? parseFloat(row[options.scoreColumn]) || 0\n : Date.now();\n pipeline.zadd(targetKey, score, val);\n break;\n }\n }\n\n await pipeline.exec();\n totalInserted += batch.length;\n }\n\n const elapsedMs = Math.max(1, Date.now() - startTime);\n const opsPerSec = Math.round((totalInserted / elapsedMs) * 1000);\n\n return {\n totalProcessed: rows.length,\n totalInserted,\n elapsedMs,\n opsPerSec,\n structure: options.structure,\n keysAffected: uniqueKeys.size,\n };\n}\n","import { Redis } from \"ioredis\";\n\nexport interface KeyInspection {\n key: string;\n type: string;\n count: number;\n memoryBytes?: number;\n ttl: number;\n samples: string[];\n}\n\n/**\n * Inspect a Redis key (count, type, memory, and sample items).\n */\nexport async function inspectKey(redis: Redis, key: string, sampleCount: number = 5): Promise<KeyInspection> {\n const [type, ttl] = await Promise.all([\n redis.type(key),\n redis.ttl(key),\n ]);\n\n let count = 0;\n let samples: string[] = [];\n\n switch (type) {\n case \"set\":\n count = await redis.scard(key);\n samples = await redis.srandmember(key, sampleCount) || [];\n break;\n\n case \"list\":\n count = await redis.llen(key);\n samples = await redis.lrange(key, 0, sampleCount - 1) || [];\n break;\n\n case \"hash\":\n count = await redis.hlen(key);\n const hashData = await redis.hgetall(key);\n samples = Object.entries(hashData).slice(0, sampleCount).map(([k, v]) => `${k}: ${v}`);\n break;\n\n case \"zset\":\n count = await redis.zcard(key);\n samples = await redis.zrange(key, 0, sampleCount - 1) || [];\n break;\n\n case \"string\":\n count = 1;\n const val = await redis.get(key);\n samples = val ? [val] : [];\n break;\n }\n\n let memoryBytes: number | undefined;\n try {\n const memory = await redis.memory(\"USAGE\", key) as number;\n if (typeof memory === \"number\") memoryBytes = memory;\n } catch {\n // MEMORY USAGE might not be supported on all Redis clusters/proxies\n }\n\n return {\n key,\n type,\n count,\n memoryBytes,\n ttl,\n samples,\n };\n}\n","import { RedisStructure } from \"../types.js\";\n\n/**\n * Generate ready-to-use k6 test script snippets using xk6-redis or HTTP mocks.\n */\nexport function generateK6Snippet(keyName: string, structure: RedisStructure): string {\n if (structure === \"set\") {\n return `// ==========================================\n// k6 Load Test - Redis Set Data Consumption\n// Requirements: xk6-redis (k6 run --vus 50 --duration 1m script.js)\n// ==========================================\nimport http from 'k6/http';\nimport { check, sleep } from 'k6';\nimport redis from 'k6/x/redis';\n\n// Connect to Redis\nconst client = new redis.Client('redis://localhost:6379');\n\nexport const options = {\n vus: 50,\n duration: '1m',\n};\n\nexport default function () {\n // Option A: Pop unique token (guarantees NO collision between VUs)\n const uniqueToken = client.spop('${keyName}');\n\n // Option B: Random sample (if token is reusable across VUs)\n // const uniqueToken = client.srandmember('${keyName}');\n\n if (!uniqueToken) {\n console.warn('Redis set \"${keyName}\" is exhausted!');\n return;\n }\n\n // Execute authenticated HTTP request using seeded token\n const res = http.get('https://api.example.com/v1/user/profile', {\n headers: {\n Authorization: \\`Bearer \\${uniqueToken}\\`,\n },\n });\n\n check(res, {\n 'status is 200': (r) => r.status === 200,\n });\n\n sleep(0.1);\n}\n`;\n }\n\n if (structure === \"list\") {\n return `// ==========================================\n// k6 Load Test - Redis Queue / List FIFO\n// Requirements: xk6-redis\n// ==========================================\nimport http from 'k6/http';\nimport { check } from 'k6';\nimport redis from 'k6/x/redis';\n\nconst client = new redis.Client('redis://localhost:6379');\n\nexport default function () {\n // Pop from FIFO queue\n const item = client.lpop('${keyName}');\n\n if (!item) {\n console.warn('Queue \"${keyName}\" is empty!');\n return;\n }\n\n const payload = JSON.parse(item);\n const res = http.post('https://api.example.com/v1/orders', JSON.stringify(payload), {\n headers: { 'Content-Type': 'application/json' },\n });\n\n check(res, { 'status is 201': (r) => r.status === 201 });\n}\n`;\n }\n\n // Default string / hash snippet\n return `// ==========================================\n// k6 Load Test - Redis Key-Value Fetch\n// Requirements: xk6-redis\n// ==========================================\nimport http from 'k6/http';\nimport { check } from 'k6';\nimport redis from 'k6/x/redis';\n\nconst client = new redis.Client('redis://localhost:6379');\n\nexport default function () {\n const value = client.get('${keyName}');\n const res = http.get(\\`https://api.example.com/v1/item/\\${value}\\`);\n check(res, { 'status is 200': (r) => r.status === 200 });\n}\n`;\n}\n"],"mappings":"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","names":["fs","picocolors"]}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import picocolors from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/loaders/file.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { parse } from "csv-parse/sync";
|
|
10
|
+
function parseCSV(content, delimiter = ",") {
|
|
11
|
+
const records = parse(content, {
|
|
12
|
+
columns: true,
|
|
13
|
+
skip_empty_lines: true,
|
|
14
|
+
trim: true,
|
|
15
|
+
delimiter,
|
|
16
|
+
cast: false
|
|
17
|
+
});
|
|
18
|
+
return records;
|
|
19
|
+
}
|
|
20
|
+
function parseJSONOrJSONL(content) {
|
|
21
|
+
const trimmed = content.trim();
|
|
22
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
23
|
+
const parsed = JSON.parse(trimmed);
|
|
24
|
+
if (!Array.isArray(parsed)) throw new Error("JSON root must be an array of objects.");
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
const lines = trimmed.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (line.startsWith("{") && line.endsWith("}")) {
|
|
31
|
+
rows.push(JSON.parse(line));
|
|
32
|
+
} else {
|
|
33
|
+
rows.push({ value: line });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
function parseLines(content) {
|
|
39
|
+
return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l) => ({ value: l }));
|
|
40
|
+
}
|
|
41
|
+
function loadAndParseFile(filePath) {
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
throw new Error(`File not found: ${filePath}`);
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
46
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
47
|
+
if (ext === "csv") {
|
|
48
|
+
return parseCSV(content);
|
|
49
|
+
} else if (ext === "tsv") {
|
|
50
|
+
return parseCSV(content, " ");
|
|
51
|
+
} else if (ext === "json" || ext === "jsonl" || ext === "ndjson") {
|
|
52
|
+
return parseJSONOrJSONL(content);
|
|
53
|
+
} else if (ext === "txt") {
|
|
54
|
+
return parseLines(content);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return parseJSONOrJSONL(content);
|
|
58
|
+
} catch {
|
|
59
|
+
try {
|
|
60
|
+
return parseCSV(content);
|
|
61
|
+
} catch {
|
|
62
|
+
return parseLines(content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/engine/client.ts
|
|
68
|
+
import { Redis } from "ioredis";
|
|
69
|
+
function createRedisClient(options = {}) {
|
|
70
|
+
const url = options.url || process.env.REDIS_URL || process.env.REDIS_URI;
|
|
71
|
+
if (url) {
|
|
72
|
+
return new Redis(url, {
|
|
73
|
+
lazyConnect: false,
|
|
74
|
+
maxRetriesPerRequest: 3,
|
|
75
|
+
enableReadyCheck: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const redisConfig = {
|
|
79
|
+
host: options.host || process.env.REDIS_HOST || "127.0.0.1",
|
|
80
|
+
port: options.port || (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379),
|
|
81
|
+
password: options.password || process.env.REDIS_PASSWORD || void 0,
|
|
82
|
+
db: options.db !== void 0 ? options.db : process.env.REDIS_DB ? parseInt(process.env.REDIS_DB, 10) : 0,
|
|
83
|
+
maxRetriesPerRequest: 3,
|
|
84
|
+
lazyConnect: false
|
|
85
|
+
};
|
|
86
|
+
if (options.tls) {
|
|
87
|
+
redisConfig.tls = {};
|
|
88
|
+
}
|
|
89
|
+
return new Redis(redisConfig);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/utils/template.ts
|
|
93
|
+
function renderTemplate(template, row) {
|
|
94
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_-]+)\s*\}\}/g, (_, key) => {
|
|
95
|
+
return row[key] !== void 0 ? String(row[key]) : "";
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function extractRowValue(row, column, valueTemplate) {
|
|
99
|
+
if (valueTemplate) {
|
|
100
|
+
return renderTemplate(valueTemplate, row);
|
|
101
|
+
}
|
|
102
|
+
if (column && row[column] !== void 0) {
|
|
103
|
+
return String(row[column]);
|
|
104
|
+
}
|
|
105
|
+
const keys = Object.keys(row);
|
|
106
|
+
if (keys.length === 1) {
|
|
107
|
+
return String(row[keys[0]]);
|
|
108
|
+
}
|
|
109
|
+
return JSON.stringify(row);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/engine/seeder.ts
|
|
113
|
+
async function executeSeed(redis, rows, options) {
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
const batchSize = options.batchSize || 1e3;
|
|
116
|
+
let totalInserted = 0;
|
|
117
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
118
|
+
if (options.flushFirst && options.keyName && !options.keyName.includes("{{")) {
|
|
119
|
+
await redis.del(options.keyName);
|
|
120
|
+
}
|
|
121
|
+
for (let i = 0; i < rows.length; i += batchSize) {
|
|
122
|
+
const batch = rows.slice(i, i + batchSize);
|
|
123
|
+
const pipeline = redis.pipeline();
|
|
124
|
+
for (const row of batch) {
|
|
125
|
+
const targetKey = options.keyName ? renderTemplate(options.keyName, row) : "seed:data";
|
|
126
|
+
uniqueKeys.add(targetKey);
|
|
127
|
+
const val = extractRowValue(row, options.column, options.valueTemplate);
|
|
128
|
+
switch (options.structure) {
|
|
129
|
+
case "set":
|
|
130
|
+
pipeline.sadd(targetKey, val);
|
|
131
|
+
break;
|
|
132
|
+
case "list":
|
|
133
|
+
pipeline.rpush(targetKey, val);
|
|
134
|
+
break;
|
|
135
|
+
case "string":
|
|
136
|
+
if (options.ttl) {
|
|
137
|
+
pipeline.set(targetKey, val, "EX", options.ttl);
|
|
138
|
+
} else {
|
|
139
|
+
pipeline.set(targetKey, val);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
case "hash":
|
|
143
|
+
const hashFields = {};
|
|
144
|
+
for (const [k, v] of Object.entries(row)) {
|
|
145
|
+
hashFields[k] = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
146
|
+
}
|
|
147
|
+
pipeline.hset(targetKey, hashFields);
|
|
148
|
+
if (options.ttl) {
|
|
149
|
+
pipeline.expire(targetKey, options.ttl);
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case "zset":
|
|
153
|
+
const score = options.scoreColumn && row[options.scoreColumn] !== void 0 ? parseFloat(row[options.scoreColumn]) || 0 : Date.now();
|
|
154
|
+
pipeline.zadd(targetKey, score, val);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await pipeline.exec();
|
|
159
|
+
totalInserted += batch.length;
|
|
160
|
+
}
|
|
161
|
+
const elapsedMs = Math.max(1, Date.now() - startTime);
|
|
162
|
+
const opsPerSec = Math.round(totalInserted / elapsedMs * 1e3);
|
|
163
|
+
return {
|
|
164
|
+
totalProcessed: rows.length,
|
|
165
|
+
totalInserted,
|
|
166
|
+
elapsedMs,
|
|
167
|
+
opsPerSec,
|
|
168
|
+
structure: options.structure,
|
|
169
|
+
keysAffected: uniqueKeys.size
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/engine/inspector.ts
|
|
174
|
+
async function inspectKey(redis, key, sampleCount = 5) {
|
|
175
|
+
const [type, ttl] = await Promise.all([
|
|
176
|
+
redis.type(key),
|
|
177
|
+
redis.ttl(key)
|
|
178
|
+
]);
|
|
179
|
+
let count = 0;
|
|
180
|
+
let samples = [];
|
|
181
|
+
switch (type) {
|
|
182
|
+
case "set":
|
|
183
|
+
count = await redis.scard(key);
|
|
184
|
+
samples = await redis.srandmember(key, sampleCount) || [];
|
|
185
|
+
break;
|
|
186
|
+
case "list":
|
|
187
|
+
count = await redis.llen(key);
|
|
188
|
+
samples = await redis.lrange(key, 0, sampleCount - 1) || [];
|
|
189
|
+
break;
|
|
190
|
+
case "hash":
|
|
191
|
+
count = await redis.hlen(key);
|
|
192
|
+
const hashData = await redis.hgetall(key);
|
|
193
|
+
samples = Object.entries(hashData).slice(0, sampleCount).map(([k, v]) => `${k}: ${v}`);
|
|
194
|
+
break;
|
|
195
|
+
case "zset":
|
|
196
|
+
count = await redis.zcard(key);
|
|
197
|
+
samples = await redis.zrange(key, 0, sampleCount - 1) || [];
|
|
198
|
+
break;
|
|
199
|
+
case "string":
|
|
200
|
+
count = 1;
|
|
201
|
+
const val = await redis.get(key);
|
|
202
|
+
samples = val ? [val] : [];
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
let memoryBytes;
|
|
206
|
+
try {
|
|
207
|
+
const memory = await redis.memory("USAGE", key);
|
|
208
|
+
if (typeof memory === "number") memoryBytes = memory;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
key,
|
|
213
|
+
type,
|
|
214
|
+
count,
|
|
215
|
+
memoryBytes,
|
|
216
|
+
ttl,
|
|
217
|
+
samples
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/utils/k6.ts
|
|
222
|
+
function generateK6Snippet(keyName, structure) {
|
|
223
|
+
if (structure === "set") {
|
|
224
|
+
return `// ==========================================
|
|
225
|
+
// k6 Load Test - Redis Set Data Consumption
|
|
226
|
+
// Requirements: xk6-redis (k6 run --vus 50 --duration 1m script.js)
|
|
227
|
+
// ==========================================
|
|
228
|
+
import http from 'k6/http';
|
|
229
|
+
import { check, sleep } from 'k6';
|
|
230
|
+
import redis from 'k6/x/redis';
|
|
231
|
+
|
|
232
|
+
// Connect to Redis
|
|
233
|
+
const client = new redis.Client('redis://localhost:6379');
|
|
234
|
+
|
|
235
|
+
export const options = {
|
|
236
|
+
vus: 50,
|
|
237
|
+
duration: '1m',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export default function () {
|
|
241
|
+
// Option A: Pop unique token (guarantees NO collision between VUs)
|
|
242
|
+
const uniqueToken = client.spop('${keyName}');
|
|
243
|
+
|
|
244
|
+
// Option B: Random sample (if token is reusable across VUs)
|
|
245
|
+
// const uniqueToken = client.srandmember('${keyName}');
|
|
246
|
+
|
|
247
|
+
if (!uniqueToken) {
|
|
248
|
+
console.warn('Redis set "${keyName}" is exhausted!');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Execute authenticated HTTP request using seeded token
|
|
253
|
+
const res = http.get('https://api.example.com/v1/user/profile', {
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: \`Bearer \${uniqueToken}\`,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
check(res, {
|
|
260
|
+
'status is 200': (r) => r.status === 200,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
sleep(0.1);
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
if (structure === "list") {
|
|
268
|
+
return `// ==========================================
|
|
269
|
+
// k6 Load Test - Redis Queue / List FIFO
|
|
270
|
+
// Requirements: xk6-redis
|
|
271
|
+
// ==========================================
|
|
272
|
+
import http from 'k6/http';
|
|
273
|
+
import { check } from 'k6';
|
|
274
|
+
import redis from 'k6/x/redis';
|
|
275
|
+
|
|
276
|
+
const client = new redis.Client('redis://localhost:6379');
|
|
277
|
+
|
|
278
|
+
export default function () {
|
|
279
|
+
// Pop from FIFO queue
|
|
280
|
+
const item = client.lpop('${keyName}');
|
|
281
|
+
|
|
282
|
+
if (!item) {
|
|
283
|
+
console.warn('Queue "${keyName}" is empty!');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const payload = JSON.parse(item);
|
|
288
|
+
const res = http.post('https://api.example.com/v1/orders', JSON.stringify(payload), {
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
check(res, { 'status is 201': (r) => r.status === 201 });
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
return `// ==========================================
|
|
297
|
+
// k6 Load Test - Redis Key-Value Fetch
|
|
298
|
+
// Requirements: xk6-redis
|
|
299
|
+
// ==========================================
|
|
300
|
+
import http from 'k6/http';
|
|
301
|
+
import { check } from 'k6';
|
|
302
|
+
import redis from 'k6/x/redis';
|
|
303
|
+
|
|
304
|
+
const client = new redis.Client('redis://localhost:6379');
|
|
305
|
+
|
|
306
|
+
export default function () {
|
|
307
|
+
const value = client.get('${keyName}');
|
|
308
|
+
const res = http.get(\`https://api.example.com/v1/item/\${value}\`);
|
|
309
|
+
check(res, { 'status is 200': (r) => r.status === 200 });
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/cli.ts
|
|
315
|
+
async function main() {
|
|
316
|
+
const program = new Command();
|
|
317
|
+
program.name("rediseed").description(
|
|
318
|
+
"Blazing-fast Redis data seeder & loader from CSV, JSON, and line streams for k6 load testing & staging fixtures."
|
|
319
|
+
).version("1.0.0", "-v, --version", "Output current version");
|
|
320
|
+
program.option("-u, --url <url>", "Redis connection URL (e.g. redis://localhost:6379 or rediss:// for TLS)").option("-h, --host <host>", "Redis host", "127.0.0.1").option("-p, --port <port>", "Redis port", "6379").option("-a, --password <password>", "Redis auth password").option("-n, --db <db>", "Redis database index (0-15)", "0").option("--tls", "Enable TLS connection", false);
|
|
321
|
+
program.command("seed <file>").description("Load and seed data from CSV, JSON, or TXT file into Redis").option("-s, --structure <type>", "Redis structure: set, list, hash, string, zset", "set").option("-k, --key <name>", "Destination Redis key name or template (e.g. 'k6:user_ids' or 'user:{{id}}')", "seed:data").option("-c, --column <col>", "Extract specific column/property (for set, list, string)").option("--value <template>", "Value template for string/hash (e.g. '{{email}}:{{token}}')").option("--score-column <col>", "Column for zset score (defaults to timestamp)").option("-b, --batch-size <size>", "Pipeline batch size", "1000").option("--ttl <seconds>", "TTL expiration in seconds for keys").option("--flush", "Delete destination key before inserting", false).option("--dry-run", "Preview parsing and first 5 rows without writing to Redis", false).action(async (file, options) => {
|
|
322
|
+
try {
|
|
323
|
+
const globalOpts = program.opts();
|
|
324
|
+
console.log(picocolors.bold(`
|
|
325
|
+
\u{1F331} rediseed \u2014 Loading data from: ${picocolors.cyan(file)}`));
|
|
326
|
+
const rows = loadAndParseFile(file);
|
|
327
|
+
console.log(` \u2022 Parsed ${picocolors.green(rows.length.toLocaleString())} records.`);
|
|
328
|
+
if (options.dryRun) {
|
|
329
|
+
console.log(picocolors.yellow("\n[DRY RUN PREVIEW - First 5 items]"));
|
|
330
|
+
for (const row of rows.slice(0, 5)) {
|
|
331
|
+
console.log(" ", JSON.stringify(row));
|
|
332
|
+
}
|
|
333
|
+
console.log(picocolors.dim(`
|
|
334
|
+
Would insert ${rows.length} items into Redis structure '${options.structure}' with key '${options.key}'.
|
|
335
|
+
`));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const redis = createRedisClient({
|
|
339
|
+
url: globalOpts.url,
|
|
340
|
+
host: globalOpts.host,
|
|
341
|
+
port: parseInt(globalOpts.port, 10),
|
|
342
|
+
password: globalOpts.password,
|
|
343
|
+
db: parseInt(globalOpts.db, 10),
|
|
344
|
+
tls: globalOpts.tls
|
|
345
|
+
});
|
|
346
|
+
const result = await executeSeed(redis, rows, {
|
|
347
|
+
structure: options.structure,
|
|
348
|
+
keyName: options.key,
|
|
349
|
+
column: options.column,
|
|
350
|
+
valueTemplate: options.value,
|
|
351
|
+
scoreColumn: options.scoreColumn,
|
|
352
|
+
batchSize: parseInt(options.batchSize, 10),
|
|
353
|
+
flushFirst: options.flush,
|
|
354
|
+
ttl: options.ttl ? parseInt(options.ttl, 10) : void 0
|
|
355
|
+
});
|
|
356
|
+
await redis.quit();
|
|
357
|
+
console.log(picocolors.green(`
|
|
358
|
+
\u2714 Seeding completed successfully!`));
|
|
359
|
+
console.log(` \u2022 Structure: ${picocolors.cyan(result.structure.toUpperCase())}`);
|
|
360
|
+
console.log(` \u2022 Target Key: ${picocolors.cyan(options.key)}`);
|
|
361
|
+
console.log(` \u2022 Total Items: ${picocolors.bold(result.totalInserted.toLocaleString())}`);
|
|
362
|
+
console.log(` \u2022 Speed: ${picocolors.bold(result.opsPerSec.toLocaleString())} ops/sec (${result.elapsedMs}ms)
|
|
363
|
+
`);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(picocolors.red(`\u2716 Error: ${err.message || err}`));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
program.command("set <keyName> <file>").description("Shortcut to load a file column directly into a Redis Set (ideal for k6 SPOP unique IDs)").option("-c, --column <col>", "Extract specific column name from CSV/JSON").option("--flush", "Delete set before inserting", false).action(async (keyName, file, options) => {
|
|
370
|
+
try {
|
|
371
|
+
const globalOpts = program.opts();
|
|
372
|
+
const rows = loadAndParseFile(file);
|
|
373
|
+
const redis = createRedisClient({
|
|
374
|
+
url: globalOpts.url,
|
|
375
|
+
host: globalOpts.host,
|
|
376
|
+
port: parseInt(globalOpts.port, 10),
|
|
377
|
+
password: globalOpts.password,
|
|
378
|
+
db: parseInt(globalOpts.db, 10),
|
|
379
|
+
tls: globalOpts.tls
|
|
380
|
+
});
|
|
381
|
+
const result = await executeSeed(redis, rows, {
|
|
382
|
+
structure: "set",
|
|
383
|
+
keyName,
|
|
384
|
+
column: options.column,
|
|
385
|
+
flushFirst: options.flush
|
|
386
|
+
});
|
|
387
|
+
await redis.quit();
|
|
388
|
+
console.log(picocolors.green(`
|
|
389
|
+
\u2714 Successfully loaded ${result.totalInserted.toLocaleString()} unique items into Redis Set: ${picocolors.cyan(keyName)} (${result.elapsedMs}ms)`));
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(picocolors.red(`\u2716 Error: ${err.message || err}`));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
program.command("inspect <key>").description("Inspect item count, data type, memory usage, and samples from a Redis key").option("-s, --samples <count>", "Number of sample items to preview", "5").action(async (key, options) => {
|
|
396
|
+
try {
|
|
397
|
+
const globalOpts = program.opts();
|
|
398
|
+
const redis = createRedisClient({
|
|
399
|
+
url: globalOpts.url,
|
|
400
|
+
host: globalOpts.host,
|
|
401
|
+
port: parseInt(globalOpts.port, 10),
|
|
402
|
+
password: globalOpts.password,
|
|
403
|
+
db: parseInt(globalOpts.db, 10),
|
|
404
|
+
tls: globalOpts.tls
|
|
405
|
+
});
|
|
406
|
+
const info = await inspectKey(redis, key, parseInt(options.samples, 10));
|
|
407
|
+
await redis.quit();
|
|
408
|
+
console.log(`
|
|
409
|
+
${picocolors.bold("\u{1F50D} Redis Key Inspector")}`);
|
|
410
|
+
console.log(` \u2022 Key: ${picocolors.cyan(info.key)}`);
|
|
411
|
+
console.log(` \u2022 Type: ${picocolors.green(info.type.toUpperCase())}`);
|
|
412
|
+
console.log(` \u2022 Total Items: ${picocolors.bold(info.count.toLocaleString())}`);
|
|
413
|
+
console.log(` \u2022 TTL: ${info.ttl === -1 ? "No Expiration" : `${info.ttl}s`}`);
|
|
414
|
+
if (info.memoryBytes) {
|
|
415
|
+
console.log(` \u2022 Memory Usage: ${(info.memoryBytes / 1024).toFixed(2)} KB`);
|
|
416
|
+
}
|
|
417
|
+
console.log(`
|
|
418
|
+
${picocolors.bold("Sample Items:")}`);
|
|
419
|
+
for (const sample of info.samples) {
|
|
420
|
+
console.log(` \u2022 ${sample}`);
|
|
421
|
+
}
|
|
422
|
+
console.log("");
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.error(picocolors.red(`\u2716 Error: ${err.message || err}`));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
program.command("k6 <key>").description("Generate ready-to-use k6 load test script snippet consuming this Redis key").option("-s, --structure <type>", "Redis structure (set, list, string)", "set").action((key, options) => {
|
|
429
|
+
const snippet = generateK6Snippet(key, options.structure);
|
|
430
|
+
console.log(`
|
|
431
|
+
${snippet}
|
|
432
|
+
`);
|
|
433
|
+
});
|
|
434
|
+
await program.parseAsync(process.argv);
|
|
435
|
+
}
|
|
436
|
+
main().catch((err) => {
|
|
437
|
+
console.error(err);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
});
|
|
440
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/loaders/file.ts","../src/engine/client.ts","../src/utils/template.ts","../src/engine/seeder.ts","../src/engine/inspector.ts","../src/utils/k6.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport picocolors from \"picocolors\";\nimport { loadAndParseFile } from \"./loaders/file.js\";\nimport { createRedisClient } from \"./engine/client.js\";\nimport { executeSeed } from \"./engine/seeder.js\";\nimport { inspectKey } from \"./engine/inspector.js\";\nimport { generateK6Snippet } from \"./utils/k6.js\";\nimport { RedisStructure } from \"./types.js\";\n\nasync function main() {\n const program = new Command();\n\n program\n .name(\"rediseed\")\n .description(\n \"Blazing-fast Redis data seeder & loader from CSV, JSON, and line streams for k6 load testing & staging fixtures.\"\n )\n .version(\"1.0.0\", \"-v, --version\", \"Output current version\");\n\n // Global Redis Options\n program\n .option(\"-u, --url <url>\", \"Redis connection URL (e.g. redis://localhost:6379 or rediss:// for TLS)\")\n .option(\"-h, --host <host>\", \"Redis host\", \"127.0.0.1\")\n .option(\"-p, --port <port>\", \"Redis port\", \"6379\")\n .option(\"-a, --password <password>\", \"Redis auth password\")\n .option(\"-n, --db <db>\", \"Redis database index (0-15)\", \"0\")\n .option(\"--tls\", \"Enable TLS connection\", false);\n\n // 1. Seed Command\n program\n .command(\"seed <file>\")\n .description(\"Load and seed data from CSV, JSON, or TXT file into Redis\")\n .option(\"-s, --structure <type>\", \"Redis structure: set, list, hash, string, zset\", \"set\")\n .option(\"-k, --key <name>\", \"Destination Redis key name or template (e.g. 'k6:user_ids' or 'user:{{id}}')\", \"seed:data\")\n .option(\"-c, --column <col>\", \"Extract specific column/property (for set, list, string)\")\n .option(\"--value <template>\", \"Value template for string/hash (e.g. '{{email}}:{{token}}')\")\n .option(\"--score-column <col>\", \"Column for zset score (defaults to timestamp)\")\n .option(\"-b, --batch-size <size>\", \"Pipeline batch size\", \"1000\")\n .option(\"--ttl <seconds>\", \"TTL expiration in seconds for keys\")\n .option(\"--flush\", \"Delete destination key before inserting\", false)\n .option(\"--dry-run\", \"Preview parsing and first 5 rows without writing to Redis\", false)\n .action(async (file, options) => {\n try {\n const globalOpts = program.opts();\n console.log(picocolors.bold(`\\nš± rediseed ā Loading data from: ${picocolors.cyan(file)}`));\n\n const rows = loadAndParseFile(file);\n console.log(` ⢠Parsed ${picocolors.green(rows.length.toLocaleString())} records.`);\n\n if (options.dryRun) {\n console.log(picocolors.yellow(\"\\n[DRY RUN PREVIEW - First 5 items]\"));\n for (const row of rows.slice(0, 5)) {\n console.log(\" \", JSON.stringify(row));\n }\n console.log(picocolors.dim(`\\nWould insert ${rows.length} items into Redis structure '${options.structure}' with key '${options.key}'.\\n`));\n return;\n }\n\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const result = await executeSeed(redis, rows, {\n structure: options.structure as RedisStructure,\n keyName: options.key,\n column: options.column,\n valueTemplate: options.value,\n scoreColumn: options.scoreColumn,\n batchSize: parseInt(options.batchSize, 10),\n flushFirst: options.flush,\n ttl: options.ttl ? parseInt(options.ttl, 10) : undefined,\n });\n\n await redis.quit();\n\n console.log(picocolors.green(`\\nā Seeding completed successfully!`));\n console.log(` ⢠Structure: ${picocolors.cyan(result.structure.toUpperCase())}`);\n console.log(` ⢠Target Key: ${picocolors.cyan(options.key)}`);\n console.log(` ⢠Total Items: ${picocolors.bold(result.totalInserted.toLocaleString())}`);\n console.log(` ⢠Speed: ${picocolors.bold(result.opsPerSec.toLocaleString())} ops/sec (${result.elapsedMs}ms)\\n`);\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 2. Set Shortcut Command (seed directly into a Redis Set for k6 SPOP)\n program\n .command(\"set <keyName> <file>\")\n .description(\"Shortcut to load a file column directly into a Redis Set (ideal for k6 SPOP unique IDs)\")\n .option(\"-c, --column <col>\", \"Extract specific column name from CSV/JSON\")\n .option(\"--flush\", \"Delete set before inserting\", false)\n .action(async (keyName, file, options) => {\n try {\n const globalOpts = program.opts();\n const rows = loadAndParseFile(file);\n\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const result = await executeSeed(redis, rows, {\n structure: \"set\",\n keyName,\n column: options.column,\n flushFirst: options.flush,\n });\n\n await redis.quit();\n\n console.log(picocolors.green(`\\nā Successfully loaded ${result.totalInserted.toLocaleString()} unique items into Redis Set: ${picocolors.cyan(keyName)} (${result.elapsedMs}ms)`));\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 3. Inspect Command\n program\n .command(\"inspect <key>\")\n .description(\"Inspect item count, data type, memory usage, and samples from a Redis key\")\n .option(\"-s, --samples <count>\", \"Number of sample items to preview\", \"5\")\n .action(async (key, options) => {\n try {\n const globalOpts = program.opts();\n const redis = createRedisClient({\n url: globalOpts.url,\n host: globalOpts.host,\n port: parseInt(globalOpts.port, 10),\n password: globalOpts.password,\n db: parseInt(globalOpts.db, 10),\n tls: globalOpts.tls,\n });\n\n const info = await inspectKey(redis, key, parseInt(options.samples, 10));\n await redis.quit();\n\n console.log(`\\n${picocolors.bold(\"š Redis Key Inspector\")}`);\n console.log(` ⢠Key: ${picocolors.cyan(info.key)}`);\n console.log(` ⢠Type: ${picocolors.green(info.type.toUpperCase())}`);\n console.log(` ⢠Total Items: ${picocolors.bold(info.count.toLocaleString())}`);\n console.log(` ⢠TTL: ${info.ttl === -1 ? \"No Expiration\" : `${info.ttl}s`}`);\n if (info.memoryBytes) {\n console.log(` ⢠Memory Usage: ${(info.memoryBytes / 1024).toFixed(2)} KB`);\n }\n console.log(`\\n${picocolors.bold(\"Sample Items:\")}`);\n for (const sample of info.samples) {\n console.log(` ⢠${sample}`);\n }\n console.log(\"\");\n } catch (err: any) {\n console.error(picocolors.red(`ā Error: ${err.message || err}`));\n process.exit(1);\n }\n });\n\n // 4. Generate k6 Snippet\n program\n .command(\"k6 <key>\")\n .description(\"Generate ready-to-use k6 load test script snippet consuming this Redis key\")\n .option(\"-s, --structure <type>\", \"Redis structure (set, list, string)\", \"set\")\n .action((key, options) => {\n const snippet = generateK6Snippet(key, options.structure as RedisStructure);\n console.log(`\\n${snippet}\\n`);\n });\n\n await program.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","import fs from \"fs\";\nimport { parse } from \"csv-parse/sync\";\nimport { ParsedRow } from \"../types.js\";\n\n/**\n * Parse a CSV string or file content into array of row objects.\n */\nexport function parseCSV(content: string, delimiter: string = \",\"): ParsedRow[] {\n const records = parse(content, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n delimiter,\n cast: false,\n });\n return records;\n}\n\n/**\n * Parse a JSON array, single JSON object, or JSONL string into array of row objects.\n */\nexport function parseJSONOrJSONL(content: string): ParsedRow[] {\n const trimmed = content.trim();\n if (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")) {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed)) throw new Error(\"JSON root must be an array of objects.\");\n return parsed;\n }\n\n // Check if it is line-delimited JSON (JSONL)\n const lines = trimmed.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\n const rows: ParsedRow[] = [];\n for (const line of lines) {\n if (line.startsWith(\"{\") && line.endsWith(\"}\")) {\n rows.push(JSON.parse(line));\n } else {\n // Plain text single column line\n rows.push({ value: line });\n }\n }\n\n return rows;\n}\n\n/**\n * Parse plain text lines into { value: line } rows.\n */\nexport function parseLines(content: string): ParsedRow[] {\n return content\n .split(/\\r?\\n/)\n .map(l => l.trim())\n .filter(Boolean)\n .map(l => ({ value: l }));\n}\n\n/**\n * Auto-detect file format from extension and contents.\n */\nexport function loadAndParseFile(filePath: string): ParsedRow[] {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n\n const content = fs.readFileSync(filePath, \"utf8\");\n const ext = filePath.split(\".\").pop()?.toLowerCase();\n\n if (ext === \"csv\") {\n return parseCSV(content);\n } else if (ext === \"tsv\") {\n return parseCSV(content, \"\\t\");\n } else if (ext === \"json\" || ext === \"jsonl\" || ext === \"ndjson\") {\n return parseJSONOrJSONL(content);\n } else if (ext === \"txt\") {\n return parseLines(content);\n }\n\n // Fallback try JSON then CSV then lines\n try {\n return parseJSONOrJSONL(content);\n } catch {\n try {\n return parseCSV(content);\n } catch {\n return parseLines(content);\n }\n }\n}\n","import { Redis, RedisOptions } from \"ioredis\";\nimport { RedisConnectionOptions } from \"../types.js\";\n\n/**\n * Create a reusable ioredis client instance.\n */\nexport function createRedisClient(options: RedisConnectionOptions = {}): Redis {\n const url = options.url || process.env.REDIS_URL || process.env.REDIS_URI;\n\n if (url) {\n return new Redis(url, {\n lazyConnect: false,\n maxRetriesPerRequest: 3,\n enableReadyCheck: true,\n });\n }\n\n const redisConfig: RedisOptions = {\n host: options.host || process.env.REDIS_HOST || \"127.0.0.1\",\n port: options.port || (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379),\n password: options.password || process.env.REDIS_PASSWORD || undefined,\n db: options.db !== undefined ? options.db : (process.env.REDIS_DB ? parseInt(process.env.REDIS_DB, 10) : 0),\n maxRetriesPerRequest: 3,\n lazyConnect: false,\n };\n\n if (options.tls) {\n redisConfig.tls = {};\n }\n\n return new Redis(redisConfig);\n}\n","import { ParsedRow } from \"../types.js\";\n\n/**\n * Render template string replacing `{{variable}}` with values from row.\n */\nexport function renderTemplate(template: string, row: ParsedRow): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_-]+)\\s*\\}\\}/g, (_, key) => {\n return row[key] !== undefined ? String(row[key]) : \"\";\n });\n}\n\n/**\n * Extract item value from row based on column or template.\n */\nexport function extractRowValue(row: ParsedRow, column?: string, valueTemplate?: string): string {\n if (valueTemplate) {\n return renderTemplate(valueTemplate, row);\n }\n\n if (column && row[column] !== undefined) {\n return String(row[column]);\n }\n\n // If there's only 1 property in the row, use it\n const keys = Object.keys(row);\n if (keys.length === 1) {\n return String(row[keys[0]]);\n }\n\n // Otherwise return entire row as JSON string\n return JSON.stringify(row);\n}\n","import { Redis } from \"ioredis\";\nimport { SeedOptions, SeedResult, ParsedRow } from \"../types.js\";\nimport { renderTemplate, extractRowValue } from \"../utils/template.js\";\n\n/**\n * Execute high-speed batched seeding into Redis using pipelines.\n */\nexport async function executeSeed(\n redis: Redis,\n rows: ParsedRow[],\n options: SeedOptions\n): Promise<SeedResult> {\n const startTime = Date.now();\n const batchSize = options.batchSize || 1000;\n let totalInserted = 0;\n const uniqueKeys = new Set<string>();\n\n // If flushFirst is enabled and static keyName is provided, delete it first\n if (options.flushFirst && options.keyName && !options.keyName.includes(\"{{\")) {\n await redis.del(options.keyName);\n }\n\n // Process in batches using pipeline\n for (let i = 0; i < rows.length; i += batchSize) {\n const batch = rows.slice(i, i + batchSize);\n const pipeline = redis.pipeline();\n\n for (const row of batch) {\n const targetKey = options.keyName\n ? renderTemplate(options.keyName, row)\n : \"seed:data\";\n\n uniqueKeys.add(targetKey);\n const val = extractRowValue(row, options.column, options.valueTemplate);\n\n switch (options.structure) {\n case \"set\":\n pipeline.sadd(targetKey, val);\n break;\n\n case \"list\":\n pipeline.rpush(targetKey, val);\n break;\n\n case \"string\":\n if (options.ttl) {\n pipeline.set(targetKey, val, \"EX\", options.ttl);\n } else {\n pipeline.set(targetKey, val);\n }\n break;\n\n case \"hash\":\n // If row has multiple columns, store full hash fields\n const hashFields: Record<string, string> = {};\n for (const [k, v] of Object.entries(row)) {\n hashFields[k] = typeof v === \"object\" ? JSON.stringify(v) : String(v);\n }\n pipeline.hset(targetKey, hashFields);\n if (options.ttl) {\n pipeline.expire(targetKey, options.ttl);\n }\n break;\n\n case \"zset\":\n const score = options.scoreColumn && row[options.scoreColumn] !== undefined\n ? parseFloat(row[options.scoreColumn]) || 0\n : Date.now();\n pipeline.zadd(targetKey, score, val);\n break;\n }\n }\n\n await pipeline.exec();\n totalInserted += batch.length;\n }\n\n const elapsedMs = Math.max(1, Date.now() - startTime);\n const opsPerSec = Math.round((totalInserted / elapsedMs) * 1000);\n\n return {\n totalProcessed: rows.length,\n totalInserted,\n elapsedMs,\n opsPerSec,\n structure: options.structure,\n keysAffected: uniqueKeys.size,\n };\n}\n","import { Redis } from \"ioredis\";\n\nexport interface KeyInspection {\n key: string;\n type: string;\n count: number;\n memoryBytes?: number;\n ttl: number;\n samples: string[];\n}\n\n/**\n * Inspect a Redis key (count, type, memory, and sample items).\n */\nexport async function inspectKey(redis: Redis, key: string, sampleCount: number = 5): Promise<KeyInspection> {\n const [type, ttl] = await Promise.all([\n redis.type(key),\n redis.ttl(key),\n ]);\n\n let count = 0;\n let samples: string[] = [];\n\n switch (type) {\n case \"set\":\n count = await redis.scard(key);\n samples = await redis.srandmember(key, sampleCount) || [];\n break;\n\n case \"list\":\n count = await redis.llen(key);\n samples = await redis.lrange(key, 0, sampleCount - 1) || [];\n break;\n\n case \"hash\":\n count = await redis.hlen(key);\n const hashData = await redis.hgetall(key);\n samples = Object.entries(hashData).slice(0, sampleCount).map(([k, v]) => `${k}: ${v}`);\n break;\n\n case \"zset\":\n count = await redis.zcard(key);\n samples = await redis.zrange(key, 0, sampleCount - 1) || [];\n break;\n\n case \"string\":\n count = 1;\n const val = await redis.get(key);\n samples = val ? [val] : [];\n break;\n }\n\n let memoryBytes: number | undefined;\n try {\n const memory = await redis.memory(\"USAGE\", key) as number;\n if (typeof memory === \"number\") memoryBytes = memory;\n } catch {\n // MEMORY USAGE might not be supported on all Redis clusters/proxies\n }\n\n return {\n key,\n type,\n count,\n memoryBytes,\n ttl,\n samples,\n };\n}\n","import { RedisStructure } from \"../types.js\";\n\n/**\n * Generate ready-to-use k6 test script snippets using xk6-redis or HTTP mocks.\n */\nexport function generateK6Snippet(keyName: string, structure: RedisStructure): string {\n if (structure === \"set\") {\n return `// ==========================================\n// k6 Load Test - Redis Set Data Consumption\n// Requirements: xk6-redis (k6 run --vus 50 --duration 1m script.js)\n// ==========================================\nimport http from 'k6/http';\nimport { check, sleep } from 'k6';\nimport redis from 'k6/x/redis';\n\n// Connect to Redis\nconst client = new redis.Client('redis://localhost:6379');\n\nexport const options = {\n vus: 50,\n duration: '1m',\n};\n\nexport default function () {\n // Option A: Pop unique token (guarantees NO collision between VUs)\n const uniqueToken = client.spop('${keyName}');\n\n // Option B: Random sample (if token is reusable across VUs)\n // const uniqueToken = client.srandmember('${keyName}');\n\n if (!uniqueToken) {\n console.warn('Redis set \"${keyName}\" is exhausted!');\n return;\n }\n\n // Execute authenticated HTTP request using seeded token\n const res = http.get('https://api.example.com/v1/user/profile', {\n headers: {\n Authorization: \\`Bearer \\${uniqueToken}\\`,\n },\n });\n\n check(res, {\n 'status is 200': (r) => r.status === 200,\n });\n\n sleep(0.1);\n}\n`;\n }\n\n if (structure === \"list\") {\n return `// ==========================================\n// k6 Load Test - Redis Queue / List FIFO\n// Requirements: xk6-redis\n// ==========================================\nimport http from 'k6/http';\nimport { check } from 'k6';\nimport redis from 'k6/x/redis';\n\nconst client = new redis.Client('redis://localhost:6379');\n\nexport default function () {\n // Pop from FIFO queue\n const item = client.lpop('${keyName}');\n\n if (!item) {\n console.warn('Queue \"${keyName}\" is empty!');\n return;\n }\n\n const payload = JSON.parse(item);\n const res = http.post('https://api.example.com/v1/orders', JSON.stringify(payload), {\n headers: { 'Content-Type': 'application/json' },\n });\n\n check(res, { 'status is 201': (r) => r.status === 201 });\n}\n`;\n }\n\n // Default string / hash snippet\n return `// ==========================================\n// k6 Load Test - Redis Key-Value Fetch\n// Requirements: xk6-redis\n// ==========================================\nimport http from 'k6/http';\nimport { check } from 'k6';\nimport redis from 'k6/x/redis';\n\nconst client = new redis.Client('redis://localhost:6379');\n\nexport default function () {\n const value = client.get('${keyName}');\n const res = http.get(\\`https://api.example.com/v1/item/\\${value}\\`);\n check(res, { 'status is 200': (r) => r.status === 200 });\n}\n`;\n}\n"],"mappings":"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","names":[]}
|