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==}