import { v5 as uuid } from 'uuid'; import * as seedrandom from 'seedrandom'; import { PrimaryKeyType } from '@common/types'; const SEED_UUID_NAMESPACE = 'e81a0012-568b-415c-96fa-66508f594067'; const FIVE_YEARS_IN_MILLISECONDS = 5 * 365 * 24 * 60 * 60 * 1000; type OptionsSeedGenerateBase = { quantity: number; seed?: string; vendor?: string; isDefaultValue?: boolean; }; export type OptionsSeedGeneratePrimaryKeys = OptionsSeedGenerateBase & OptionsSeedGenerateInteger; export type OptionsSeedGenerateInteger = OptionsSeedGenerateBase & { startsAt?: number; incremental?: boolean; }; export type OptionsSeedGenerateBigInteger = OptionsSeedGenerateBase; export type OptionsSeedGenerateString = OptionsSeedGenerateBase & { startsWith?: string; endsWith?: string; }; export type OptionsSeedGenerateUUID = OptionsSeedGenerateBase; export type OptionsSeedGenerateBoolean = OptionsSeedGenerateBase; export type OptionsSeedGenerateCSV = OptionsSeedGenerateBase; export type OptionsSeedGenerateDate = OptionsSeedGenerateBase & { startsFrom?: Date | string; endsOn?: Date | string; }; export type OptionsSeedGenerateDateTime = OptionsSeedGenerateTimestamp; export type OptionsSeedGenerateDecimal = OptionsSeedGenerateFloat; export type OptionsSeedGenerateFloat = OptionsSeedGenerateBase & { startsAt?: number; endsAt?: number; amount?: number; precision?: number; }; export type OptionsSeedGenerateGeometry = OptionsSeedGenerateBase; export type OptionsSeedGenerateHash = OptionsSeedGenerateBase; export type OptionsSeedGenerateJSON = OptionsSeedGenerateBase; export type OptionsSeedGenerateTime = OptionsSeedGenerateBase & { startsFrom?: Date | string; endsOn?: Date | string; }; export type OptionsSeedGenerateTimestamp = OptionsSeedGenerateBase & { startsFrom?: Date; endsOn?: Date; }; export const SeedFunctions = { generatePrimaryKeys, generateValues: { integer: generateInteger, bigInteger: generateBigInteger, uuid: generateUUID, string: generateString, text: generateString, boolean: generateBoolean, csv: generateCSV, date: generateDate, dateTime: generateDateTime, decimal: generateDecimal, float: generateFloat, // geometry: null, // hash: null, // json: null, time: generateTime, timestamp: generateTimestamp, }, }; function generatePrimaryKeys(pkType: PrimaryKeyType, options: OptionsSeedGeneratePrimaryKeys) { switch (pkType) { case 'integer': return generateInteger(options); case 'uuid': return generateUUID(options); case 'string': return generateString(options); default: throw new Error(`Unsupported primary key type: ${pkType}`); } } function generateInteger(options: OptionsSeedGenerateInteger) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; if (options.incremental) { const randomStartValue = random.int32(); for (let i = 0; i < options.quantity; i++) { if (options.startsAt) { values.push(options.startsAt + i); } else { values.push(randomStartValue + i); } } } else { for (let i = 0; i < options.quantity; i++) { if (options.startsAt) { values.push(random.int32() + options.startsAt + i); } else { values.push(random.int32() + i); } } } return values; } function generateBigInteger(options: OptionsSeedGenerateBigInteger) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; for (let i = 0; i < options.quantity; i++) { // TODO: Maximise array length back to 15, fix filter parsing which reduces this value const hexString = Array(12) .fill(0) .map(() => Math.round(random() * 0xf).toString(16)) .join(''); values.push(BigInt(`0x7${hexString}`)); } return values; } function generateFloat(options: OptionsSeedGenerateFloat) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; for (let i = 0; i < options.quantity; i++) { if (options.startsAt && options.endsAt) { values.push( Number((options.startsAt + random() * (options.endsAt - options.startsAt + 1)).toFixed(options.precision ?? 3)) ); } else if (options.startsAt) { values.push(Number((options.startsAt + random() * (options.amount ?? 100)).toFixed(options.precision ?? 3))); } else if (options.endsAt) { values.push(Number((options.endsAt - random() * (options.amount ?? 100)).toFixed(options.precision ?? 3))); } else { values.push(Number((random() * (options.amount ?? 100)).toFixed(options.precision ?? 3))); } } return values; } function generateDecimal(options: OptionsSeedGenerateDecimal) { return generateFloat(options); } function generateString(options: OptionsSeedGenerateString) { const values = []; for (let i = 0; i < options.quantity; i++) { let value = uuid(String((options.seed ?? 'default') + i), SEED_UUID_NAMESPACE).replace(/-/g, ''); if (options.startsWith) { value = options.startsWith + value; } if (options.endsWith) { value = value + options.endsWith; } values.push(value); } return values; } function generateUUID(options: OptionsSeedGenerateUUID) { const values = []; for (let i = 0; i < options.quantity; i++) { values.push(uuid(String((options.seed ?? 'default') + i), SEED_UUID_NAMESPACE)); } return values; } function generateBoolean(options: OptionsSeedGenerateBoolean) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; for (let i = 0; i < options.quantity; i++) { values.push(random() >= 0.5); } // Ensure that there is always true and false values to test on if (options.quantity >= 2) { values[0] = true; values[1] = false; } return values; } function generateCSV(options: OptionsSeedGenerateCSV) { const values = []; for (let i = 0; i < options.quantity; i++) { values.push(uuid(String((options.seed ?? 'default') + i), SEED_UUID_NAMESPACE)); } return values; } function generateDate(options: OptionsSeedGenerateDate) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; if (typeof options.startsFrom === 'string') { options.startsFrom = new Date(Date.parse(options.startsFrom)); } if (typeof options.endsOn === 'string') { options.endsOn = new Date(Date.parse(options.endsOn)); } if (options.startsFrom && options.endsOn) { for (let i = 0; i < options.quantity; i++) { values.push( new Date( Math.floor( random() * (options.endsOn.getTime() - options.startsFrom.getTime() + 1) + options.startsFrom.getTime() ) ) .toISOString() .substring(0, 10) ); } } else if (options.startsFrom) { for (let i = 0; i < options.quantity; i++) { values.push( new Date(Math.floor(random() * FIVE_YEARS_IN_MILLISECONDS + options.startsFrom.getTime())) .toISOString() .substring(0, 10) ); } } else if (options.endsOn) { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * options.endsOn.getTime())).toISOString().substring(0, 10)); } } else { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * FIVE_YEARS_IN_MILLISECONDS)).toISOString().substring(0, 10)); } } if (options.isDefaultValue && options.vendor === 'oracle') { for (let i = 0; i < values.length; i++) { values[i] = new Date(values[i]) .toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', }) .replace(/ /g, '-'); } } return values; } function generateDateTime(options: OptionsSeedGenerateDateTime) { const values = generateTimestamp(options); for (let i = 0; i < values.length; i++) { values[i] = values[i].slice(0, -5); } if (options.isDefaultValue && options.vendor === 'oracle') { for (let index = 0; index < values.length; index++) { values[index] = 'CURRENT_TIMESTAMP'; } } return values; } function generateTime(options: OptionsSeedGenerateTime) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; let timeStart = options.startsFrom; let timeEnd = options.endsOn; if (typeof timeStart === 'string') { timeStart = new Date(`1970-01-01T${timeStart}`); } if (typeof timeEnd === 'string') { timeEnd = new Date(`1970-01-01T${timeEnd}`); } if (timeStart && timeEnd) { for (let i = 0; i < options.quantity; i++) { values.push( new Date(Math.floor(random() * (timeEnd.getTime() - timeStart.getTime() + 1) + timeStart.getTime())) .toISOString() .slice(11, 19) ); } } else if (timeStart) { for (let i = 0; i < options.quantity; i++) { values.push( new Date(Math.floor(random() * (86400000 /* 24h */ - timeStart.getTime()) + timeStart.getTime())) .toISOString() .slice(11, 19) ); } } else if (timeEnd) { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * timeEnd.getTime())).toISOString().slice(11, 19)); } } else { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * FIVE_YEARS_IN_MILLISECONDS)).toISOString().slice(11, 19)); } } if (options.isDefaultValue && options.vendor === 'oracle') { for (let index = 0; index < values.length; index++) { values[index] = 'CURRENT_TIMESTAMP'; } } return values; } function generateTimestamp(options: OptionsSeedGenerateTimestamp) { const random = seedrandom.xor128(options.seed ?? 'default'); const values = []; if (options.startsFrom && options.endsOn) { for (let i = 0; i < options.quantity; i++) { values.push( new Date( Math.floor( random() * (options.endsOn.getTime() - options.startsFrom.getTime() + 1) + options.startsFrom.getTime() ) ).toISOString() ); } } else if (options.startsFrom) { for (let i = 0; i < options.quantity; i++) { values.push( new Date(Math.floor(random() * FIVE_YEARS_IN_MILLISECONDS + options.startsFrom.getTime())).toISOString() ); } } else if (options.endsOn) { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * options.endsOn.getTime())).toISOString()); } } else { for (let i = 0; i < options.quantity; i++) { values.push(new Date(Math.floor(random() * FIVE_YEARS_IN_MILLISECONDS)).toISOString()); } } // Overcome MySQL / Maria created without decimal accuracy // Overcome MSSQL specific accuracy up to 1/300th of a second for (let index = 0; index < values.length; index++) { values[index] = values[index].slice(0, 20) + '000Z'; } if (options.isDefaultValue && options.vendor) { if (['mysql', 'mysql5', 'maria'].includes(options.vendor)) { for (let index = 0; index < values.length; index++) { values[index] = new Date(values[index]).toISOString().replace(/([^T]+)T([^.]+).*/g, '$1 $2'); } } else if (options.vendor === 'oracle') { for (let index = 0; index < values.length; index++) { values[index] = 'CURRENT_TIMESTAMP'; } } } return values; }