mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(cli): integrate with command line (#526)
This commit is contained in:
parent
32e9afe047
commit
72aa2f53e6
6 changed files with 124 additions and 21 deletions
|
@ -37,6 +37,8 @@
|
||||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/schemas": "^0.1.0",
|
"@logto/schemas": "^0.1.0",
|
||||||
|
"chalk": "^4",
|
||||||
|
"commander": "^9.1.0",
|
||||||
"decamelize": "^5.0.0",
|
"decamelize": "^5.0.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"roarr": "^7.11.0",
|
"roarr": "^7.11.0",
|
||||||
|
|
73
packages/cli/src/commands/database/index.ts
Normal file
73
packages/cli/src/commands/database/index.ts
Normal file
|
@ -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 <address>',
|
||||||
|
`Your Postgres DSN *WITHOUT* database name.\n${versionNote}`
|
||||||
|
)
|
||||||
|
.env('DSN')
|
||||||
|
.makeOptionMandatory(),
|
||||||
|
domain: new Option('--domain <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<string, unknown>) => {
|
||||||
|
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<string, unknown>) => {
|
||||||
|
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<string, unknown>) => {
|
||||||
|
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<string, unknown>) => {
|
||||||
|
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;
|
|
@ -2,27 +2,22 @@ import { readdir, readFile } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { seeds } from '@logto/schemas';
|
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 { createInterceptors } from 'slonik-interceptor-preset';
|
||||||
import { raw } from 'slonik-sql-tag-raw';
|
import { raw } from 'slonik-sql-tag-raw';
|
||||||
|
|
||||||
import { insertInto } from './utilities';
|
import { insertInto, replaceDsnDatabase } from './utilities';
|
||||||
|
|
||||||
const { managementResource, defaultSignInExperience, createDefaultSetting } = seeds;
|
const { managementResource, defaultSignInExperience, createDefaultSetting } = seeds;
|
||||||
const tableDirectory = 'node_modules/@logto/schemas/tables';
|
const tableDirectory = 'node_modules/@logto/schemas/tables';
|
||||||
const domain = 'http://localhost:3001';
|
|
||||||
const defaultDatabase = 'logto_test';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a database.
|
* Create a database.
|
||||||
* @returns DSN with the created database name.
|
* @returns DSN with the created database name.
|
||||||
*/
|
*/
|
||||||
export const createDatabase = async (
|
export const createDatabase = async (dsn: string, databaseName: string): Promise<string> => {
|
||||||
dsn: string,
|
const pool = createPool(replaceDsnDatabase(dsn, 'postgres'));
|
||||||
databaseName = defaultDatabase
|
|
||||||
): Promise<string> => {
|
|
||||||
const { databaseName: _, ...restDsn } = parseDsn(dsn);
|
|
||||||
const pool = createPool(dsn);
|
|
||||||
|
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
create database ${sql.identifier([databaseName])}
|
create database ${sql.identifier([databaseName])}
|
||||||
|
@ -32,8 +27,11 @@ export const createDatabase = async (
|
||||||
lc_ctype = 'en_US.utf8'
|
lc_ctype = 'en_US.utf8'
|
||||||
connection_limit = -1;
|
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) => {
|
export const createDatabaseCli = (dsn: string) => {
|
||||||
|
@ -53,21 +51,18 @@ export const createDatabaseCli = (dsn: string) => {
|
||||||
for (const [file, query] of queries) {
|
for (const [file, query] of queries) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await pool.query(sql`${raw(query)}`);
|
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([
|
await Promise.all([
|
||||||
pool.query(insertInto(managementResource, 'resources')),
|
pool.query(insertInto(managementResource, 'resources')),
|
||||||
pool.query(insertInto(createDefaultSetting(domain), 'settings')),
|
pool.query(insertInto(createDefaultSetting(domain), 'settings')),
|
||||||
pool.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
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 ?? '');
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { SchemaLike, SchemaValue, SchemaValuePrimitive } from '@logto/schemas';
|
import { SchemaLike, SchemaValue, SchemaValuePrimitive } from '@logto/schemas';
|
||||||
import decamelize from 'decamelize';
|
import decamelize from 'decamelize';
|
||||||
import { sql, SqlToken } from 'slonik';
|
import { parseDsn, sql, SqlToken, stringifyDsn } from 'slonik';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note `undefined` is removed from the acceptable list,
|
* Note `undefined` is removed from the acceptable list,
|
||||||
|
@ -52,3 +52,6 @@ export const insertInto = <T extends SchemaLike>(object: T, table: string) => {
|
||||||
)})
|
)})
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const replaceDsnDatabase = (dsn: string, databaseName: string): string =>
|
||||||
|
stringifyDsn({ ...parseDsn(dsn), databaseName });
|
|
@ -1,5 +1,13 @@
|
||||||
|
import { Command } from 'commander';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
import database from './commands/database';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
export * from './database';
|
const program = new Command();
|
||||||
|
|
||||||
|
program.addCommand(database);
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
// Export * from './database';
|
||||||
|
|
|
@ -24,6 +24,8 @@ importers:
|
||||||
'@silverhand/eslint-config': ^0.10.2
|
'@silverhand/eslint-config': ^0.10.2
|
||||||
'@silverhand/ts-config': ^0.10.2
|
'@silverhand/ts-config': ^0.10.2
|
||||||
'@types/node': '14'
|
'@types/node': '14'
|
||||||
|
chalk: ^4
|
||||||
|
commander: ^9.1.0
|
||||||
decamelize: ^5.0.0
|
decamelize: ^5.0.0
|
||||||
dotenv: ^10.0.0
|
dotenv: ^10.0.0
|
||||||
eslint: ^8.10.0
|
eslint: ^8.10.0
|
||||||
|
@ -37,6 +39,8 @@ importers:
|
||||||
typescript: ^4.6.3
|
typescript: ^4.6.3
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
|
chalk: 4.1.2
|
||||||
|
commander: 9.1.0
|
||||||
decamelize: 5.0.1
|
decamelize: 5.0.1
|
||||||
dotenv: 10.0.0
|
dotenv: 10.0.0
|
||||||
roarr: 7.11.0
|
roarr: 7.11.0
|
||||||
|
@ -7693,6 +7697,11 @@ packages:
|
||||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
engines: {node: '>= 12'}
|
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:
|
/commondir/1.0.1:
|
||||||
resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=}
|
resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -14489,7 +14498,7 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
shell-quote: 1.7.3
|
shell-quote: 1.7.3
|
||||||
yargs: 17.3.1
|
yargs: 17.4.1
|
||||||
|
|
||||||
/pg-int8/1.0.1:
|
/pg-int8/1.0.1:
|
||||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
|
@ -19602,6 +19611,19 @@ packages:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.0.1
|
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:
|
/ylru/1.2.1:
|
||||||
resolution: {integrity: sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==}
|
resolution: {integrity: sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==}
|
||||||
|
|
Loading…
Reference in a new issue