Skip to content

Instantly share code, notes, and snippets.

@thataustin
Last active November 17, 2024 03:12
Show Gist options
  • Select an option

  • Save thataustin/339f3f9e6cb4187597f74580cedddde4 to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown
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