import { existsSync } from 'fs'; import { writeFile } from 'fs/promises'; import path from 'path'; /** * @example * console.log(sanitizeObjectKey("oneTwo")); // oneTwo * console.log(sanitizeObjectKey("one-two")); // 'one-two' * console.log(sanitizeObjectKey("_one$Two3")); // _one$Two3 * console.log(sanitizeObjectKey("3invalid")); // '3invalid' */ const sanitizeObjectKey = (key)=>{ // Regular expression for a valid identifier const identifierRegex = /^[a-z_$][\w$]*$/i; if (identifierRegex.test(key)) { return key; } return `'${key}'`; }; /** * @example * (columns default-valuesID) -> columns['default-valuesID'] * (columns defaultValues) -> columns.defaultValues */ const accessProperty = (objName, key)=>{ const sanitized = sanitizeObjectKey(key); if (sanitized.startsWith("'")) { return `${objName}[${sanitized}]`; } return `${objName}.${key}`; }; export const createSchemaGenerator = ({ columnToCodeConverter, corePackageSuffix, defaultOutputFile, enumImport, schemaImport, tableImport })=>{ return async function generateSchema({ log = true, outputFile = defaultOutputFile, prettify = true } = {}) { const importDeclarations = {}; const tableDeclarations = []; const enumDeclarations = []; const relationsDeclarations = []; const addImport = (from, name)=>{ if (!importDeclarations[from]) { importDeclarations[from] = new Set(); } importDeclarations[from].add(name); }; const corePackage = `${this.packageName}/drizzle/${corePackageSuffix}`; let schemaDeclaration = null; if (this.schemaName) { addImport(corePackage, schemaImport); schemaDeclaration = `export const db_schema = ${schemaImport}('${this.schemaName}')`; } const enumFn = this.schemaName ? `db_schema.enum` : enumImport; const enumsList = []; const addEnum = (name, options)=>{ if (enumsList.some((each)=>each === name)) { return; } enumsList.push(name); enumDeclarations.push(`export const ${name} = ${enumFn}('${name}', [${options.map((option)=>`'${option}'`).join(', ')}])`); }; if (this.payload.config.localization && enumImport) { addEnum('enum__locales', this.payload.config.localization.localeCodes); } const tableFn = this.schemaName ? `db_schema.table` : tableImport; if (!this.schemaName) { addImport(corePackage, tableImport); } addImport(corePackage, 'index'); addImport(corePackage, 'uniqueIndex'); addImport(corePackage, 'foreignKey'); addImport(`${this.packageName}/drizzle`, 'sql'); addImport(`${this.packageName}/drizzle`, 'relations'); for(const tableName in this.rawTables){ const table = this.rawTables[tableName]; const extrasDeclarations = []; if (table.indexes) { for(const key in table.indexes){ const index = table.indexes[key]; let indexDeclaration = `${index.unique ? 'uniqueIndex' : 'index'}('${index.name}')`; indexDeclaration += `.on(${typeof index.on === 'string' ? `${accessProperty('columns', index.on)}` : `${index.on.map((on)=>`${accessProperty('columns', on)}`).join(', ')}`}),`; extrasDeclarations.push(indexDeclaration); } } if (table.foreignKeys) { for(const key in table.foreignKeys){ const foreignKey = table.foreignKeys[key]; let foreignKeyDeclaration = `foreignKey({ columns: [${foreignKey.columns.map((col)=>`columns['${col}']`).join(', ')}], foreignColumns: [${foreignKey.foreignColumns.map((col)=>`${accessProperty(col.table, col.name)}`).join(', ')}], name: '${foreignKey.name}' })`; if (foreignKey.onDelete) { foreignKeyDeclaration += `.onDelete('${foreignKey.onDelete}')`; } if (foreignKey.onUpdate) { foreignKeyDeclaration += `.onUpdate('${foreignKey.onDelete}')`; } foreignKeyDeclaration += ','; extrasDeclarations.push(foreignKeyDeclaration); } } const tableCode = ` export const ${tableName} = ${tableFn}('${tableName}', { ${Object.entries(table.columns).map(([key, column])=>` ${sanitizeObjectKey(key)}: ${columnToCodeConverter({ adapter: this, addEnum, addImport, column, locales: this.payload.config.localization ? this.payload.config.localization.localeCodes : undefined, tableKey: tableName })},`).join('\n')} }${extrasDeclarations.length ? `, (columns) => [ ${extrasDeclarations.join(' ')} ]` : ''} ) `; tableDeclarations.push(tableCode); } for(const tableName in this.rawRelations){ const relations = this.rawRelations[tableName]; const properties = []; for(const key in relations){ const relation = relations[key]; let declaration; if (relation.type === 'one') { declaration = `${sanitizeObjectKey(key)}: one(${relation.to}, { ${relation.fields.some((field)=>field.table !== tableName) ? '// @ts-expect-error Drizzle TypeScript bug for ONE relationships with a field in different table' : ''} fields: [${relation.fields.map((field)=>`${accessProperty(field.table, field.name)}`).join(', ')}], references: [${relation.references.map((col)=>`${accessProperty(relation.to, col)}`).join(', ')}], ${relation.relationName ? `relationName: '${relation.relationName}',` : ''} }),`; } else { declaration = `${sanitizeObjectKey(key)}: many(${relation.to}, { ${relation.relationName ? `relationName: '${relation.relationName}',` : ''} }),`; } properties.push(declaration); } // beautify / lintify relations callback output, when no many for example, don't add it const args = []; if (Object.values(relations).some((rel)=>rel.type === 'one')) { args.push('one'); } if (Object.values(relations).some((rel)=>rel.type === 'many')) { args.push('many'); } const arg = args.length ? `{ ${args.join(', ')} }` : ''; const declaration = `export const relations_${tableName} = relations(${tableName}, (${arg}) => ({ ${properties.join('\n ')} }))`; relationsDeclarations.push(declaration); } if (enumDeclarations.length && !this.schemaName) { addImport(corePackage, enumImport); } const importDeclarationsSanitized = []; for(const moduleName in importDeclarations){ const moduleImports = importDeclarations[moduleName]; importDeclarationsSanitized.push(`import { ${Array.from(moduleImports).join(', ')} } from '${moduleName}'`); } const schemaType = ` type DatabaseSchema = { ${[ this.schemaName ? 'db_schema' : null, ...enumsList, ...Object.keys(this.rawTables), ...Object.keys(this.rawRelations).map((table)=>`relations_${table}`) ].filter(Boolean).map((name)=>`${name}: typeof ${name}`).join('\n ')} } `; const finalDeclaration = ` declare module '${this.packageName}' { export interface GeneratedDatabaseSchema { schema: DatabaseSchema } } `; const warning = ` /* tslint:disable */ /* eslint-disable */ /** * This file was automatically generated by Payload. * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, * and re-run \`payload generate:db-schema\` to regenerate this file. */ `; const importTypes = `import type {} from '${this.packageName}'`; let code = [ warning, importTypes, ...importDeclarationsSanitized, schemaDeclaration, ...enumDeclarations, ...tableDeclarations, ...relationsDeclarations, schemaType, finalDeclaration ].filter(Boolean).join('\n'); if (!outputFile) { const cwd = process.cwd(); const srcDir = path.resolve(cwd, 'src'); if (existsSync(srcDir)) { outputFile = path.resolve(srcDir, 'payload-generated-schema.ts'); } else { outputFile = path.resolve(cwd, 'payload-generated-schema.ts'); } } if (prettify) { try { const prettier = await eval('import("prettier")'); const configPath = await prettier.resolveConfigFile(); const config = configPath ? await prettier.resolveConfig(configPath) : {}; code = await prettier.format(code, { ...config, parser: 'typescript' }); } catch { /* empty */ } } await writeFile(outputFile, code, 'utf-8'); if (log) { this.payload.logger.info(`Written ${outputFile}`); } }; }; //# sourceMappingURL=createSchemaGenerator.js.map