-
Notifications
You must be signed in to change notification settings - Fork 24
fix: enhance project initialization and menu transformation capabilities #631
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
base: main
Are you sure you want to change the base?
Changes from all commits
fa3fa5c
9f0e7ab
5a5cc98
5a5786b
809e6fd
464884a
69e3adb
b9bc59d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,14 @@ function detectAdminforthVersion() { | |
| const adminforthVersion = detectAdminforthVersion(); | ||
| const SUPPORTED_DB_URL_SCHEMES = ['sqlite://', 'postgresql://', 'mongodb://', 'mysql://', 'clickhouse://']; | ||
| const PRISMA_MIGRATION_DB_PROTOCOLS = ['sqlite', 'postgres', 'postgresql', 'mysql']; | ||
| const DEFAULT_DB_URL = 'sqlite://.db.sqlite'; | ||
| const DATABASE_CONNECTOR_IMPORTS = { | ||
| sqlite: '../../dist/dataConnectors/sqlite.js', | ||
| postgresql: '../../dist/dataConnectors/postgres.js', | ||
| mysql: '../../dist/dataConnectors/mysql.js', | ||
| mongodb: '../../dist/dataConnectors/mongo.js', | ||
| clickhouse: '../../dist/dataConnectors/clickhouse.js', | ||
| }; | ||
|
|
||
|
|
||
| export function parseArgumentsIntoOptions(rawArgs) { | ||
|
|
@@ -61,6 +69,66 @@ export function parseArgumentsIntoOptions(rawArgs) { | |
| }; | ||
| } | ||
|
|
||
| function generateAdminUserTableInstructions(provider) { | ||
| if (provider === 'postgresql') { | ||
| return `\`\`\`sql | ||
| CREATE TABLE adminuser ( | ||
| id TEXT PRIMARY KEY, | ||
| email TEXT NOT NULL UNIQUE, | ||
| password_hash TEXT NOT NULL, | ||
| role TEXT NOT NULL, | ||
| created_at TIMESTAMP NOT NULL | ||
| ); | ||
| \`\`\``; | ||
| } | ||
|
|
||
| if (provider === 'mysql') { | ||
| return `\`\`\`sql | ||
| CREATE TABLE adminuser ( | ||
| id VARCHAR(191) PRIMARY KEY, | ||
| email VARCHAR(191) NOT NULL UNIQUE, | ||
| password_hash TEXT NOT NULL, | ||
| role VARCHAR(191) NOT NULL, | ||
| created_at DATETIME NOT NULL | ||
| ); | ||
| \`\`\``; | ||
| } | ||
|
|
||
| if (provider === 'sqlite') { | ||
| return `\`\`\`sql | ||
| CREATE TABLE adminuser ( | ||
| id TEXT PRIMARY KEY, | ||
| email TEXT NOT NULL UNIQUE, | ||
| password_hash TEXT NOT NULL, | ||
| role TEXT NOT NULL, | ||
| created_at DATETIME NOT NULL | ||
| ); | ||
| \`\`\``; | ||
| } | ||
|
|
||
| if (provider === 'clickhouse') { | ||
| return `\`\`\`sql | ||
| CREATE TABLE adminuser ( | ||
| id String, | ||
| email String, | ||
| password_hash String, | ||
| role String, | ||
| created_at DateTime | ||
| ) | ||
| ENGINE = MergeTree() | ||
| ORDER BY id; | ||
| \`\`\` | ||
|
|
||
| ClickHouse does not enforce UNIQUE constraints like PostgreSQL, MySQL, or SQLite. AdminForth authentication expects \`email\` values in \`adminuser\` to be unique, so enforce this in your ingestion/application logic and remove duplicate email rows to avoid ambiguous logins.`; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @NoOne7135 actually I think we don't need it here, also I think we can safely remove all unique constraints form other DBs snippets. This should be specified in AdminForth resource config already - and it takes care about all of these on own soft layer. Hard unique constraints in DB are often more pain because if u have it here, but af layer will set unique to false, users will get direct db errors, instead of clear nice message (+other reasons) - so lets leave this decision for user, if he wants to get this pain - he can add it by himself.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also - please explain to user that all these snippets are just for example, we recommend him to use his favorite migration tool first. This is just example |
||
| } | ||
|
NoOne7135 marked this conversation as resolved.
|
||
|
|
||
| if (provider === 'mongodb') { | ||
| return 'Create an `adminuser` collection with `id`, `email`, `password_hash`, `role`, and `created_at` fields. Keep `email` unique in your own schema/index setup.'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In mongo user has to do nothing ;) |
||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| export async function promptForMissingOptions(options) { | ||
| const questions = []; | ||
|
|
||
|
|
@@ -78,7 +146,7 @@ export async function promptForMissingOptions(options) { | |
| type: 'input', | ||
| name: 'db', | ||
| message: 'Please specify the database URL to use >', | ||
| default: 'sqlite://.db.sqlite', | ||
| default: DEFAULT_DB_URL, | ||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -102,10 +170,14 @@ export async function promptForMissingOptions(options) { | |
| db: options.db || answers.db, | ||
| useNpm: options.useNpm || answers.useNpm, | ||
| }; | ||
| resolvedOptions.existingDb = false; | ||
|
|
||
| await inspectDatabaseCleanState(resolvedOptions); | ||
|
|
||
| if ( | ||
| resolvedOptions.includePrismaMigrations === undefined && | ||
| isPrismaMigrationDbUrl(resolvedOptions.db) | ||
| isPrismaMigrationDbUrl(resolvedOptions.db) && | ||
| !resolvedOptions.existingDb | ||
| ) { | ||
| const prismaAnswer = await inquirer.prompt([{ | ||
| type: 'select', | ||
|
|
@@ -119,7 +191,7 @@ export async function promptForMissingOptions(options) { | |
| }]); | ||
| resolvedOptions.includePrismaMigrations = prismaAnswer.includePrismaMigrations; | ||
| } else { | ||
| resolvedOptions.includePrismaMigrations = Boolean(resolvedOptions.includePrismaMigrations); | ||
| resolvedOptions.includePrismaMigrations = Boolean(resolvedOptions.includePrismaMigrations) && !resolvedOptions.existingDb; | ||
| } | ||
|
|
||
| return resolvedOptions; | ||
|
|
@@ -189,6 +261,32 @@ function generateDbUrlForAfProd(connectionString) { | |
| return connectionString.toString(); | ||
| } | ||
|
|
||
| async function inspectDatabaseCleanState(options) { | ||
| const connectionString = parseConnectionString(options.db); | ||
| const provider = detectDbProvider(connectionString.protocol); | ||
|
|
||
| const Connector = (await import(DATABASE_CONNECTOR_IMPORTS[provider])).default; | ||
| const connector = new Connector(); | ||
|
|
||
| try { | ||
| await connector.setupClient(connectionString.toString()); | ||
| } catch (error) { | ||
| if (provider === 'sqlite' && error.message?.includes('directory does not exist')) { | ||
| options.existingDb = false; | ||
| return; | ||
| } | ||
| throw error; | ||
| } | ||
|
|
||
| try { | ||
| options.existingDb = !(await connector.isDatabaseEmpty()); | ||
| } finally { | ||
| if (typeof connector.close === 'function') { | ||
| await connector.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function initialChecks(options) { | ||
| return [ | ||
| { | ||
|
|
@@ -262,7 +360,7 @@ async function scaffoldProject(ctx, options, cwd) { | |
| const prismaDbUrlProd = generateDbUrlForPrismaProd(connectionString); | ||
|
|
||
|
|
||
| ctx.skipPrismaSetup = !prismaDbUrl; | ||
| ctx.skipPrismaSetup = !options.includePrismaMigrations || !prismaDbUrl; | ||
| const appName = options.appName; | ||
|
|
||
| const filename = fileURLToPath(import.meta.url); | ||
|
|
@@ -287,6 +385,7 @@ async function scaffoldProject(ctx, options, cwd) { | |
| prismaDbUrlProd, | ||
| appName, | ||
| provider, | ||
| existingDb: options.existingDb, | ||
| nodeMajor: parseInt(process.versions.node.split('.')[0], 10), | ||
| sqliteFile: connectionString.protocol.startsWith('sqlite') ? connectionString.host : null, | ||
| }); | ||
|
|
@@ -310,7 +409,7 @@ function getPackageManagerTemplateData(useNpm, nodeMajor) { | |
|
|
||
| async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations, options) { | ||
| const { | ||
| dbUrl, prismaDbUrl, appName, provider, nodeMajor, | ||
| dbUrl, prismaDbUrl, appName, provider, existingDb, nodeMajor, | ||
| dbUrlProd, prismaDbUrlProd, sqliteFile | ||
| } = options; | ||
| const packageManagerTemplateData = getPackageManagerTemplateData(useNpm, nodeMajor); | ||
|
|
@@ -352,7 +451,14 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations, | |
| { | ||
| src: 'readme.md.hbs', | ||
| dest: 'README.md', | ||
| data: { dbUrl, prismaDbUrl: resolvedPrismaDbUrl, appName, sqliteFile }, | ||
| data: { | ||
| dbUrl, | ||
| prismaDbUrl: resolvedPrismaDbUrl, | ||
| appName, | ||
| sqliteFile, | ||
| existingDb, | ||
| adminUserTableInstructions: existingDb ? generateAdminUserTableInstructions(provider) : null, | ||
| }, | ||
| }, | ||
| { | ||
| src: 'AGENTS.md.hbs', | ||
|
|
@@ -519,16 +625,21 @@ async function installDependenciesNpm(ctx, cwd) { | |
|
|
||
| function generateFinalInstructionsPnpm(skipPrismaSetup, options) { | ||
| let instruction = '⏭️ Run the following commands to get started:\n'; | ||
| if (!skipPrismaSetup) | ||
| instruction += ` | ||
| const provider = detectDbProvider(parseConnectionString(options.db).protocol); | ||
| instruction += ` | ||
| ${chalk.dim('// Go to the project directory')} | ||
| ${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`; | ||
|
|
||
| if (options.includePrismaMigrations && !skipPrismaSetup) | ||
| if (!skipPrismaSetup) | ||
| instruction += ` | ||
| ${chalk.dim('// Generate and apply initial migration')} | ||
| ${chalk.dim('$')}${chalk.cyan(' pnpm makemigration --name init && pnpm migrate:local')}\n`; | ||
|
|
||
| if (options.existingDb) | ||
| instruction += ` | ||
| ${chalk.dim('// Create the adminuser table in your database before starting the app')} | ||
| ${generateAdminUserTableInstructions(provider)}\n`; | ||
|
|
||
| instruction += ` | ||
| ${chalk.dim('// Start dev server with tsx watch for hot-reloading')} | ||
| ${chalk.dim('$')}${chalk.cyan(' pnpm dev')}\n | ||
|
|
@@ -541,16 +652,21 @@ function generateFinalInstructionsPnpm(skipPrismaSetup, options) { | |
|
|
||
| function generateFinalInstructionsNpm(skipPrismaSetup, options) { | ||
| let instruction = '⏭️ Run the following commands to get started:\n'; | ||
| if (!skipPrismaSetup) | ||
| instruction += ` | ||
| const provider = detectDbProvider(parseConnectionString(options.db).protocol); | ||
| instruction += ` | ||
| ${chalk.dim('// Go to the project directory')} | ||
| ${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`; | ||
|
|
||
| if (options.includePrismaMigrations && !skipPrismaSetup) | ||
| if (!skipPrismaSetup) | ||
| instruction += ` | ||
| ${chalk.dim('// Generate and apply initial migration')} | ||
| ${chalk.dim('$')}${chalk.cyan(' npm run makemigration -- --name init && npm run migrate:local')}\n`; | ||
|
|
||
| if (options.existingDb) | ||
| instruction += ` | ||
| ${chalk.dim('// Create the adminuser table in your database before starting the app')} | ||
| ${generateAdminUserTableInstructions(provider)}\n`; | ||
|
|
||
| instruction += ` | ||
| ${chalk.dim('// Start dev server with tsx watch for hot-reloading')} | ||
| ${chalk.dim('$')}${chalk.cyan(' npm run dev')}\n | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,11 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS | |
| }; | ||
| }); | ||
| } | ||
|
|
||
| async isDatabaseEmpty(): Promise<boolean> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think prisma for mongo has no sense at all. Never. It works with mongo - yes. But not for migrations - this is for typesafe prisma client which we dont use anyway! |
||
| const collections = await this.client.db().listCollections({}, { nameOnly: true }).toArray(); | ||
| return collections.every((collection) => collection.name.startsWith('system.')); | ||
| } | ||
|
|
||
|
|
||
| async discoverFields(resource) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.