Skip to content

Instantly share code, notes, and snippets.

@thataustin
Last active May 9, 2024 16:50
Show Gist options
  • Save thataustin/339f3f9e6cb4187597f74580cedddde4 to your computer and use it in GitHub Desktop.
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
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)
@thataustin
Copy link
Author

thataustin commented May 6, 2024

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

// schema.ts drizzle file
export const personTable = main.table(
    'PersonTable',
    {
        // By default, serial types don't get a value in the output object because the DB should be inserting those anyway
        id: serial('id').primaryKey().notNull(),
        name: varchar('name', { length: 255 }).notNull(), // will get assigned faker.lorem.sentence() (see typeToFaker method at the top)

         /// FAKER: faker.internet.email()
        email: varchar('email', { length: 255 }),
        
        // You can use any value other than undefined, so true or false works if you want your tests to default to something different than your DB for whatever reason :shrug: 
        /// FAKER: true
        isInvestor: boolean('isInvestor').default(false),

        /// FAKER: faker.person.bio()
        bio: varchar('bio', { length: 255 }),
        
        // You absolutely don't have to use three slashes, I just like to because it denotes that it's a special type of comment
        // FAKER:faker.location.city()
        location: varchar('location', { length: 255 }),

        // I set this to undefined because in most of my tests, I don't want this field to have a value to start
        /// FAKER:undefined
        startedFetchingDealsAt: timestamp('startedFetchingDealsAt', {
            precision: 6,
            withTimezone: true,
            mode: 'string',
        }),
    },
    (table) => { }
);
// generated fakeSchema.ts
export function fakePersonTable() {
    return {
        id: faker.number.int(),
        name: faker.lorem.sentence(),
        email: faker.internet.email(), // Custom faker method from schema comment
        bio: faker.person.bio(),
        location: faker.location.city(),
        isInvestor: true,
        startedFetchingDealsAt: undefined
    };
}

Running the Script

Copy this file to a file called generateFakeSchema.ts next to your drizzle schema.ts
Execute with ts-node in your project directory:

npx ts-node path/to/generateFakeSchema.ts

Example:

npx ts-node src/drizzle/generateFakeSchema.ts # assuming your schema.ts is in /src/drizzle/schema.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment