From 72aa2f53e65677e050be3c2af7336f9d87d3b04b Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 11 Apr 2022 16:25:57 +0800 Subject: [PATCH] feat(cli): integrate with command line (#526) --- packages/cli/package.json | 2 + packages/cli/src/commands/database/index.ts | 73 +++++++++++++++++++ .../database/library.ts} | 31 ++++---- .../src/{ => commands/database}/utilities.ts | 5 +- packages/cli/src/index.ts | 10 ++- pnpm-lock.yaml | 24 +++++- 6 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 packages/cli/src/commands/database/index.ts rename packages/cli/src/{database.ts => commands/database/library.ts} (69%) rename packages/cli/src/{ => commands/database}/utilities.ts (89%) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1625a3049..92269b523 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,8 @@ "prettier": "@silverhand/eslint-config/.prettierrc", "dependencies": { "@logto/schemas": "^0.1.0", + "chalk": "^4", + "commander": "^9.1.0", "decamelize": "^5.0.0", "dotenv": "^10.0.0", "roarr": "^7.11.0", diff --git a/packages/cli/src/commands/database/index.ts b/packages/cli/src/commands/database/index.ts new file mode 100644 index 000000000..c8dcd173d --- /dev/null +++ b/packages/cli/src/commands/database/index.ts @@ -0,0 +1,73 @@ +import { Argument, Command, Option } from 'commander'; + +import { createDatabase, createDatabaseCli } from './library'; +import { replaceDsnDatabase } from './utilities'; + +const versionNote = 'Note Logto requires Postgres v14 or higher.'; +const database = new Command('database'); + +const args = { + database: new Argument('[database]', 'The Logto database name.').default('logto'), +}; + +const options = { + dsn: new Option( + '-d --dsn
', + `Your Postgres DSN *WITHOUT* database name.\n${versionNote}` + ) + .env('DSN') + .makeOptionMandatory(), + domain: new Option('--domain ', 'The Logto domain for table seeding.') + .env('DOMAIN') + .makeOptionMandatory(), +}; + +database.alias('db'); + +database + .command('create') + .description('Create a Logto databse.') + .addArgument(args.database) + .addOption(options.dsn) + .action(async (databaseName: unknown, options: Record) => { + await createDatabase(String(options.dsn), String(databaseName)); + }); + +database + .command('create-tables') + .description('Create Logto database tables without data.') + .addArgument(args.database) + .addOption(options.dsn) + .action(async (databaseName: unknown, options: Record) => { + const cli = createDatabaseCli(replaceDsnDatabase(String(options.dsn), String(databaseName))); + await cli.createTables(); + await cli.end(); + }); + +database + .command('seed-tables') + .description('Seed tables with necessary data to run Logto.') + .addArgument(args.database) + .addOption(options.dsn) + .addOption(options.domain) + .action(async (databaseName: unknown, options: Record) => { + const cli = createDatabaseCli(replaceDsnDatabase(String(options.dsn), String(databaseName))); + await cli.seedTables(String(options.domain)); + await cli.end(); + }); + +database + .command('init') + .description(`Create and initialize a Logto database with tables and data in the DSN.`) + .addArgument(args.database) + .addOption(options.dsn) + .addOption(options.domain) + .action(async (databaseName: unknown, options: Record) => { + const dsn = await createDatabase(String(options.dsn), String(databaseName)); + const cli = createDatabaseCli(dsn); + await cli.createTables(); + await cli.seedTables(String(options.domain)); + await cli.end(); + }); + +export default database; diff --git a/packages/cli/src/database.ts b/packages/cli/src/commands/database/library.ts similarity index 69% rename from packages/cli/src/database.ts rename to packages/cli/src/commands/database/library.ts index 0f323f53c..f0eb7378d 100644 --- a/packages/cli/src/database.ts +++ b/packages/cli/src/commands/database/library.ts @@ -2,27 +2,22 @@ import { readdir, readFile } from 'fs/promises'; import path from 'path'; import { seeds } from '@logto/schemas'; -import { createPool, parseDsn, sql, stringifyDsn } from 'slonik'; +import chalk from 'chalk'; +import { createPool, sql } from 'slonik'; import { createInterceptors } from 'slonik-interceptor-preset'; import { raw } from 'slonik-sql-tag-raw'; -import { insertInto } from './utilities'; +import { insertInto, replaceDsnDatabase } from './utilities'; const { managementResource, defaultSignInExperience, createDefaultSetting } = seeds; const tableDirectory = 'node_modules/@logto/schemas/tables'; -const domain = 'http://localhost:3001'; -const defaultDatabase = 'logto_test'; /** * Create a database. * @returns DSN with the created database name. */ -export const createDatabase = async ( - dsn: string, - databaseName = defaultDatabase -): Promise => { - const { databaseName: _, ...restDsn } = parseDsn(dsn); - const pool = createPool(dsn); +export const createDatabase = async (dsn: string, databaseName: string): Promise => { + const pool = createPool(replaceDsnDatabase(dsn, 'postgres')); await pool.query(sql` create database ${sql.identifier([databaseName])} @@ -32,8 +27,11 @@ export const createDatabase = async ( lc_ctype = 'en_US.utf8' connection_limit = -1; `); + await pool.end(); - return stringifyDsn({ ...restDsn, databaseName }); + console.log(`${chalk.blue('[create]')} Database ${databaseName} successfully created.`); + + return replaceDsnDatabase(dsn, databaseName); }; export const createDatabaseCli = (dsn: string) => { @@ -53,21 +51,18 @@ export const createDatabaseCli = (dsn: string) => { for (const [file, query] of queries) { // eslint-disable-next-line no-await-in-loop await pool.query(sql`${raw(query)}`); - console.log(`Create Tables: Run ${file} succeeded.`); + console.log(`${chalk.blue('[create-tables]')} Run ${file} succeeded.`); } }; - const seedTables = async () => { + const seedTables = async (domain: string) => { await Promise.all([ pool.query(insertInto(managementResource, 'resources')), pool.query(insertInto(createDefaultSetting(domain), 'settings')), pool.query(insertInto(defaultSignInExperience, 'sign_in_experiences')), ]); - console.log('Seed Tables: Seed tables succeeded.'); + console.log(`${chalk.blue('[seed-tables]')} Seed tables succeeded.`); }; - return { createTables, seedTables }; + return { createTables, seedTables, end: pool.end }; }; - -// For testing purpose, will remove later -void createDatabase(process.env.DSN ?? ''); diff --git a/packages/cli/src/utilities.ts b/packages/cli/src/commands/database/utilities.ts similarity index 89% rename from packages/cli/src/utilities.ts rename to packages/cli/src/commands/database/utilities.ts index 9affa2ced..2a7813ded 100644 --- a/packages/cli/src/utilities.ts +++ b/packages/cli/src/commands/database/utilities.ts @@ -2,7 +2,7 @@ import { SchemaLike, SchemaValue, SchemaValuePrimitive } from '@logto/schemas'; import decamelize from 'decamelize'; -import { sql, SqlToken } from 'slonik'; +import { parseDsn, sql, SqlToken, stringifyDsn } from 'slonik'; /** * Note `undefined` is removed from the acceptable list, @@ -52,3 +52,6 @@ export const insertInto = (object: T, table: string) => { )}) `; }; + +export const replaceDsnDatabase = (dsn: string, databaseName: string): string => + stringifyDsn({ ...parseDsn(dsn), databaseName }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 83e6e368c..0ce1ae309 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,13 @@ +import { Command } from 'commander'; import dotenv from 'dotenv'; +import database from './commands/database'; + dotenv.config(); -export * from './database'; +const program = new Command(); + +program.addCommand(database); +program.parse(process.argv); + +// Export * from './database'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afbf0fa80..d58e16f6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,8 @@ importers: '@silverhand/eslint-config': ^0.10.2 '@silverhand/ts-config': ^0.10.2 '@types/node': '14' + chalk: ^4 + commander: ^9.1.0 decamelize: ^5.0.0 dotenv: ^10.0.0 eslint: ^8.10.0 @@ -37,6 +39,8 @@ importers: typescript: ^4.6.3 dependencies: '@logto/schemas': link:../schemas + chalk: 4.1.2 + commander: 9.1.0 decamelize: 5.0.1 dotenv: 10.0.0 roarr: 7.11.0 @@ -7693,6 +7697,11 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + /commander/9.1.0: + resolution: {integrity: sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==} + engines: {node: ^12.20.0 || >=14} + dev: false + /commondir/1.0.1: resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} dev: false @@ -14489,7 +14498,7 @@ packages: hasBin: true dependencies: shell-quote: 1.7.3 - yargs: 17.3.1 + yargs: 17.4.1 /pg-int8/1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -19602,6 +19611,19 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.0.1 + dev: true + + /yargs/17.4.1: + resolution: {integrity: sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==} + engines: {node: '>=12'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.0.1 /ylru/1.2.1: resolution: {integrity: sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==}