Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema first approach with generated client #3484

Open
3 tasks
B4nan opened this issue Sep 11, 2022 · 0 comments
Open
3 tasks

Schema first approach with generated client #3484

B4nan opened this issue Sep 11, 2022 · 0 comments
Labels
enhancement New feature or request

Comments

@B4nan
Copy link
Member

B4nan commented Sep 11, 2022

Original idea coming from this tweet: https://twitter.com/MikroOrm/status/1568534840370237441

Prerequisities

  • generate simple repositories next to the entities (in separate files, linking them via EntityRepositoryType symbol)
  • allow to generate JS/D.TS, again in separate files - for code gen we would have to go with this approach probably
  • in the generated repository, we should extend the driver package, for that we will need a way to get package name and driver class (there is no way currently)

Or maybe we dong need to generate the repositories for the schema first approach? They are empty classes in the end, its only important if provided by user - we should have a way to set a custom repository without providing custom entity implementation/extension.

The generated client PoC

Last step would be that "generated client". I have this simple script I used for the PoC, it generates entities, repositories and a client file which initializes the ORM and exports a map of services:

import { MikroORM } from '@mikro-orm/core';
import { dirname } from 'node:path';
import fs from 'fs-extra';
import { fileURLToPath } from 'node:url';

const orm = await MikroORM.init();
const dirName = dirname(fileURLToPath(import.meta.url));
const driver = { package: '@mikro-orm/better-sqlite', className: 'BetterSqliteDriver' };

// clean up everything in the folder except dot files
await fs.emptyDir(dirName + '/entities');
await fs.emptyDir(dirName + '/repositories');

const entityGenerator = orm.config.get('entityGenerator');
entityGenerator.identifiedReferences = true;
entityGenerator.bidirectionalRelations = true;
// orm.config.set('entityGenerator', entityGenerator);
const ret = await orm.entityGenerator.generate({
  baseDir: dirName + '/entities',
  save: true,
});

const metadata = orm.getMetadata().getAll();
const entities = Object.values(metadata).filter(meta => !meta.pivotTable && !meta.embeddable && !meta.virtual);

for (const meta of entities) {
  const code = [
    `import { EntityRepository } from '${driver.package}';`,
    `import { ${meta.className} } from '../entities/${meta.className}.js';`,
    '',
    `export class ${meta.className}Repository extends EntityRepository<${meta.className}> { }`,
  ];
  await fs.writeFile(`${dirName}/repositories/${meta.className}Repository.ts`, code.join('\n'));
}

const client: string[] = [];
const coreImports: string[] = [];

client.push(`import { ${driver.className} } from '${driver.package}';`);

for (const meta of entities) {
  client.push(`import { ${meta.className} } from './entities/${meta.className}.js';`);
  client.push(`import { ${meta.className}Repository } from './repositories/${meta.className}Repository.js';`);
}

client.push('');
coreImports.push('MikroORM');
client.push(`const orm = await MikroORM.init<${driver.className}>();`);
// we will need something like this, but that itself won't allow extension, we would have to check if something was
// discovered, discover the generated entities only if they are not provided and discovered already. user would extend
// the generated entities, this means we might have to mark them as base entities somehow, to get around duplicity warnings
client.push(`await orm.discoverEntity([${entities.map(meta => meta.className).join(', ')}]);`);
client.push(`const client = {`);
client.push(`  orm,`);
client.push(`  em: orm.em,`);
client.push(`  schema: orm.schema,`);
client.push(`  seeder: orm.seeder,`);
client.push(`  migrator: orm.migrator,`);

function lcfirst(word: string) {
  return word[0].toLowerCase() + word.substring(1);
}

for (const meta of entities) {
  client.push(`  ${lcfirst(meta.className)}: orm.em.getRepository(${meta.className}) as ${meta.className}Repository,`);
}

client.push(`};`);
client.push(`export default client;`);
client.push('');

client.unshift(`import { ${coreImports.join(', ')} } from '@mikro-orm/core';`);

console.log(client.join('\n'));
await fs.writeFile(dirName + '/client.ts', client.join('\n'));

console.info('Database client generated');
await orm.close();

This would work only when used with the generated entities, important bit here is finding a way how to make this work dynamically with user provided entitites (that would extend the generated ones).

We also need to think about non-ESM projects, without top level await support, and in general the top level await is not suited for switching configurations (dev/prod). A callback would be better here, we can detect the type of project and generate it based on that. A callback might be generally better, we need to think about how this would be actually used.

The important followup is finding a way to allow extension of the generated entities, probably via some ORM configuration, so the generator already knows the link, therefore knows what to discover. Same needs to work for repositories, but the generated ones don't bring any real benefit, so its enough to support user provided ones (might work out of box, user provided entity links user provided repository).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant