Last active
November 17, 2024 03:12
-
-
Save thataustin/339f3f9e6cb4187597f74580cedddde4 to your computer and use it in GitHub Desktop.
Create fakeSchema.ts file from your drizzle schema.ts file to automatically get objects with faker data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ts from 'typescript' | |
import { readFile, stat, unlink, writeFile } from 'fs/promises' | |
import * as path from 'path' | |
import { faker } from '@faker-js/faker' | |
interface Column { | |
columnType: string | |
default?: any | |
fakeMethod?: string // Optional custom faker method | |
} | |
interface TableSchema { | |
[key: string]: Column | |
} | |
export const defaultFakerMethods: Record<string, string | undefined | boolean> = | |
{ | |
PgBigInt53: 'faker.number.bigInt().toString()', | |
PgBigInt64: 'faker.number.bigInt().toString()', | |
PgBigSerial53: 'faker.string.sample(1)', | |
PgBigSerial64: 'faker.string.sample(1)', | |
PgBoolean: false, | |
PgChar: 'faker.string.sample(1)', | |
PgCidr: 'faker.string.sample(1)', | |
PgArray: 'faker.string.sample(1)', | |
PgColumn: 'faker.string.sample(1)', | |
PgCustomColumn: 'faker.string.sample(1)', | |
PgDate: 'faker.date.recent().toISOString()', | |
PgDateString: 'faker.date.recent().toISOString()', | |
PgDoublePrecision: 'faker.number.float()', | |
PgEnumColumn: 'faker.string.sample(1)', | |
PgInet: 'faker.string.sample(1)', | |
PgInteger: 'faker.number.int({max: 9999})', | |
PgInterval: 'faker.string.sample(1)', | |
PgJson: 'JSON.stringify(faker.datatype.json())', | |
PgJsonb: 'JSON.stringify(faker.datatype.json())', | |
PgMacaddr: 'faker.string.sample(1)', | |
PgMacaddr8: 'faker.string.sample(1)', | |
PgNumeric: 'faker.number.int({max: 9999})', | |
PgReal: 'faker.number.int({max: 9999})', | |
PgSerial: undefined, // better to let the DB insert these | |
PgSmallInt: 'faker.number.int({max: 999})', | |
PgSmallSerial: undefined, // better to let the DB insert these | |
PgText: 'faker.lorem.sentence()', | |
PgTime: 'faker.date.soon().toISOString()', | |
PgTimestamp: 'faker.date.soon().toISOString()', | |
PgTimestampString: 'faker.date.soon().toISOString()', | |
PgUUID: 'faker.string.uuid()', | |
PgVarchar: 'faker.lorem.sentence()', | |
} | |
function extractComments(node: ts.Node, sourceFile: ts.SourceFile): string[] { | |
const comments: string[] = [] | |
const ranges = | |
ts.getLeadingCommentRanges(sourceFile.text, node.getFullStart()) || [] | |
ranges.forEach((range) => { | |
if (!range) { | |
return | |
} | |
if ( | |
!!range && | |
(range?.kind === ts.SyntaxKind.SingleLineCommentTrivia || | |
range?.kind === ts.SyntaxKind.MultiLineCommentTrivia) | |
) { | |
const comment = sourceFile.text.substring(range.pos, range.end) | |
comments.push(comment) | |
} | |
}) | |
return comments | |
} | |
function generateFakeFunction( | |
tableSchema: TableSchema, | |
tableName: string, | |
sourceFile: ts.SourceFile | |
): string { | |
const camelCaseTableName = | |
tableName.charAt(0).toUpperCase() + tableName.slice(1) | |
let functionString = `export function fake${camelCaseTableName}() {\n return {\n` | |
let columnsAdded = 0 | |
Object.entries(tableSchema).forEach(([key, column]) => { | |
if (!column || !column.columnType) { | |
// console.log(`Skipping ${key} because column is ${column}`) | |
return | |
} | |
const methodFromCodeComments = findFakeMethodForColumn( | |
sourceFile, | |
tableName, | |
key | |
) | |
if (!methodFromCodeComments && column.default !== undefined) { | |
return console.log( | |
`Skipping ${tableName}::${key} because column has a default of ${JSON.stringify(column.default, null, 2)}` | |
) | |
} else if (methodFromCodeComments) { | |
console.log(`Found ${methodFromCodeComments} from code comments`) | |
} | |
const fakerMethodCallString = | |
methodFromCodeComments || defaultFakerMethods[column.columnType] | |
if (fakerMethodCallString !== undefined) { | |
functionString += ` ${key}: ${fakerMethodCallString},\n` | |
columnsAdded++ | |
} | |
}) | |
functionString += ' };\n}\n' | |
return columnsAdded > 0 ? functionString : '' | |
} | |
export async function compileTypeScript( | |
sourceFile: string, | |
outputFile: string | |
): Promise<void> { | |
const source = await readFile(sourceFile, { encoding: 'utf8' }) | |
const result = ts.transpileModule(source, { | |
compilerOptions: { | |
module: ts.ModuleKind.CommonJS, | |
target: ts.ScriptTarget.ESNext, | |
}, | |
}) | |
return await writeFile(outputFile, result.outputText) | |
} | |
async function processSchemaFile( | |
inputFile: string, | |
outputFile: string | |
): Promise<void> { | |
const compiledJsSchemaFilePath = path.join( | |
path.dirname(inputFile), | |
'__compiledSchema.js' | |
) | |
// Compile TypeScript to JavaScript | |
try { | |
await compileTypeScript(inputFile, compiledJsSchemaFilePath) | |
} catch (error) { | |
console.error('Compilation failed:', error) | |
return | |
} | |
// Check if the compiled file exists | |
try { | |
const fileExists = await stat(compiledJsSchemaFilePath) | |
if (!fileExists) { | |
console.error('Compiled JS file does not exist.') | |
return | |
} | |
} catch (error) { | |
console.error('Error checking compiled JS file:', error) | |
return | |
} | |
// Assuming the compiled JS can be dynamically imported after checking its existence | |
const schemaDefinitions = await import(compiledJsSchemaFilePath) | |
const sourceFile = await getTypescriptSourceFile(inputFile) | |
let outputString = `import { faker } from '@faker-js/faker'\n` | |
// Assuming `schemaDefinitions` are properly typed, you would have validation or type casting here | |
outputString += Object.entries(schemaDefinitions) | |
.map(([tableName, tableSchema]) => | |
generateFakeFunction( | |
tableSchema as TableSchema, | |
tableName, | |
sourceFile | |
) | |
) | |
.join('\n') | |
await writeFile(outputFile, outputString) | |
// Cleanup: Delete the compiled JS file | |
try { | |
await unlink(compiledJsSchemaFilePath) | |
console.log('Temporary compiled file deleted successfully.') | |
} catch (error) { | |
console.error('Error deleting temporary file:', error) | |
} | |
} | |
let typeScriptFile: ts.SourceFile | |
async function getTypescriptSourceFile( | |
filePath: string | |
): Promise<ts.SourceFile> { | |
if (typeScriptFile) { | |
return typeScriptFile | |
} | |
const sourceText = await readFile(filePath, { encoding: 'utf8' }) | |
typeScriptFile = ts.createSourceFile( | |
filePath, | |
sourceText, | |
ts.ScriptTarget.Latest, | |
true | |
) | |
return typeScriptFile | |
} | |
function findFakeMethodForColumn( | |
sourceFile: ts.SourceFile, | |
tableName: string, | |
columnName: string | |
): string | undefined { | |
let fakeMethod: string | undefined | |
const visitNode = (node: ts.Node): void => { | |
if (ts.isVariableStatement(node)) { | |
node.declarationList.declarations.forEach((declaration) => { | |
if ( | |
ts.isVariableDeclaration(declaration) && | |
declaration.initializer | |
) { | |
const possibleTableName = declaration.name.getText() | |
if ( | |
possibleTableName === tableName && | |
ts.isCallExpression(declaration.initializer) | |
) { | |
const args = declaration.initializer.arguments | |
if ( | |
args.length > 1 && | |
ts.isObjectLiteralExpression(args[1]) | |
) { | |
args[1].properties.forEach((prop) => { | |
if ( | |
ts.isPropertyAssignment(prop) && | |
prop.name.getText() === columnName | |
) { | |
const comments = extractComments( | |
prop, | |
sourceFile | |
) | |
const fakeComment = comments.find( | |
(comment) => comment.includes('FAKER:') | |
) | |
if (fakeComment) { | |
// Capture everything after 'FAKER:' including possible whitespace that needs trimming. | |
const regex = /FAKER:(.+)/ | |
const matches = regex.exec(fakeComment) | |
if (matches && matches[1]) { | |
console.log( | |
`setting fakeMethod to matches[1] ${tableName}, ${columnName}`, | |
JSON.stringify( | |
matches[1], | |
null, | |
2 | |
) | |
) | |
// Trim any leading or trailing whitespace from the captured group | |
fakeMethod = matches[1].trim() | |
} | |
} | |
} | |
}) | |
} | |
} | |
} | |
}) | |
} | |
ts.forEachChild(node, visitNode) | |
} | |
visitNode(sourceFile) | |
return fakeMethod | |
} | |
// Example usage | |
processSchemaFile( | |
path.join(__dirname, './schema.ts'), | |
path.join(__dirname, './fakeSchema.ts') | |
) | |
.then(() => console.log('Processing completed successfully.')) | |
.catch(console.error) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Drizzle Schema Faker Generator (for Postgres, but easy to modify for other DBs, just update the
typeToFaker
hash)Inspired by https://github.com/luisrudge/prisma-generator-fake-data
But for Drizzle
Automatically generates TypeScript faker functions for Drizzle schema files by interpreting
FAKER:
comments for custom faker methods, or using default mappings based on PostgreSQL column types.Example
Running the Script
Copy this file to a file called
generateFakeSchema.ts
next to your drizzleschema.ts
Execute with
ts-node
in your project directory:Example:
npx ts-node src/drizzle/generateFakeSchema.ts # assuming your schema.ts is in /src/drizzle/schema.ts