0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #2045 from logto-io/gao-log-4311-cli-db-key-command

feat(cli): get/set db config key
This commit is contained in:
Gao Sun 2022-10-08 16:22:19 +08:00 committed by GitHub
commit 7412faa728
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 277 additions and 24 deletions

View file

@ -22,7 +22,7 @@
"precommit": "lint-staged",
"build": "rimraf lib && tsc",
"start": "node .",
"dev": "ts-node src/index.ts",
"dev": "ts-node --files src/index.ts",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"prepack": "pnpm build"
@ -34,6 +34,7 @@
"url": "https://github.com/logto-io/logto/issues"
},
"dependencies": {
"@logto/schemas": "^1.0.0-beta.10",
"chalk": "^4.1.2",
"find-up": "^5.0.0",
"got": "^11.8.2",
@ -41,6 +42,8 @@
"inquirer": "^8.2.2",
"ora": "^5.0.0",
"semver": "^7.3.7",
"slonik": "^30.0.0",
"slonik-interceptor-preset": "^1.2.10",
"tar": "^6.1.11",
"yargs": "^17.6.0",
"zod": "^3.18.0"

View file

@ -1,12 +1,14 @@
import { CommandModule } from 'yargs';
import { noop } from '../../utilities';
import { getKey, setKey } from './key';
import { getUrl, setUrl } from './url';
const database: CommandModule = {
command: ['database', 'db'],
describe: 'Commands for Logto database',
builder: (yargs) => yargs.command(getUrl).command(setUrl).strict(),
builder: (yargs) =>
yargs.command(getUrl).command(setUrl).command(getKey).command(setKey).demandCommand(1),
handler: noop,
};

View file

@ -0,0 +1,96 @@
import { logtoConfigGuards, LogtoConfigKey, logtoConfigKeys } from '@logto/schemas';
import chalk from 'chalk';
import { CommandModule } from 'yargs';
import { createPoolFromConfig } from '../../database';
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config';
import { deduplicate, log } from '../../utilities';
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));
type ValidateKeysFunction = {
(keys: string[]): asserts keys is LogtoConfigKey[];
(key: string): asserts key is LogtoConfigKey;
};
const validateKeys: ValidateKeysFunction = (keys) => {
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
// Using `.includes()` will result a type error
// eslint-disable-next-line unicorn/prefer-includes
(key) => !logtoConfigKeys.some((element) => element === key)
);
if (invalidKey) {
log.error(
`Invalid config key ${chalk.red(invalidKey)} found, expected one of ${validKeysDisplay}`
);
}
};
export const getKey: CommandModule<unknown, { key: string; keys: string[] }> = {
command: 'get-key <key> [keys...]',
describe: 'Get config value(s) of the given key(s) in Logto database',
builder: (yargs) =>
yargs
.positional('key', {
describe: `The key to get from database, one of ${validKeysDisplay}`,
type: 'string',
demandOption: true,
})
.positional('keys', {
describe: 'The additional keys to get from database',
type: 'string',
array: true,
default: [],
}),
handler: async ({ key, keys }) => {
const queryKeys = deduplicate([key, ...keys]);
validateKeys(queryKeys);
const pool = await createPoolFromConfig();
const { rows } = await getRowsByKeys(pool, queryKeys);
await pool.end();
console.log(
queryKeys
.map((currentKey) => {
const value = rows.find(({ key }) => currentKey === key)?.value;
return (
chalk.magenta(currentKey) +
'=' +
(value === undefined ? chalk.gray(value) : chalk.green(JSON.stringify(value)))
);
})
.join('\n')
);
},
};
export const setKey: CommandModule<unknown, { key: string; value: string }> = {
command: 'set-key <key> <value>',
describe: 'Set config value of the given key in Logto database',
builder: (yargs) =>
yargs
.positional('key', {
describe: `The key to get from database, one of ${validKeysDisplay}`,
type: 'string',
demandOption: true,
})
.positional('value', {
describe: 'The value to set, should be a valid JSON string',
type: 'string',
demandOption: true,
}),
handler: async ({ key, value }) => {
validateKeys(key);
const guarded = logtoConfigGuards[key].parse(JSON.parse(value));
const pool = await createPoolFromConfig();
await updateValueByKey(pool, key, guarded);
await pool.end();
log.info(`Update ${chalk.green(key)} succeeded`);
},
};

View file

@ -11,7 +11,7 @@ export const getUrl: CommandModule = {
},
};
export const setUrl: CommandModule<Record<string, unknown>, { url: string }> = {
export const setUrl: CommandModule<unknown, { url: string }> = {
command: 'set-url <url>',
describe: 'Set database URL and save to config file',
builder: (yargs) =>

View file

@ -123,7 +123,7 @@ const installLogto = async ({ path: pathArgument = defaultPath, silent = false }
);
};
const install: CommandModule<Record<string, unknown>, { path?: string; silent?: boolean }> = {
const install: CommandModule<unknown, { path?: string; silent?: boolean }> = {
command: ['init', 'i', 'install'],
describe: 'Download and run the latest Logto release',
builder: (yargs) =>

View file

@ -0,0 +1,39 @@
import chalk from 'chalk';
import { createPool, IdentifierSqlToken, sql } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import { getConfig } from './config';
import { log } from './utilities';
export const createPoolFromConfig = async () => {
const { databaseUrl } = await getConfig();
if (!databaseUrl) {
log.error(
`No database URL configured. Set one via ${chalk.green('database set-url')} command first.`
);
}
return createPool(databaseUrl, {
interceptors: createInterceptors(),
});
};
// TODO: Move database utils to `core-kit`
export type Table = { table: string; fields: Record<string, string> };
export type FieldIdentifiers<Key extends string | number | symbol> = {
[key in Key]: IdentifierSqlToken;
};
export const convertToIdentifiers = <T extends Table>({ table, fields }: T, withPrefix = false) => {
const fieldsIdentifiers = Object.entries<string>(fields).map<
[keyof T['fields'], IdentifierSqlToken]
>(([key, value]) => [key, sql.identifier(withPrefix ? [table, value] : [value])]);
return {
table: sql.identifier([table]),
// Key value inferred from the original fields directly
// eslint-disable-next-line no-restricted-syntax
fields: Object.fromEntries(fieldsIdentifiers) as FieldIdentifiers<keyof T['fields']>,
};
};

View file

@ -0,0 +1,10 @@
declare module 'slonik-interceptor-preset' {
import { Interceptor } from 'slonik';
export const createInterceptors: (config?: {
benchmarkQueries: boolean;
logQueries: boolean;
normaliseQueries: boolean;
transformFieldNames: boolean;
}) => readonly Interceptor[];
}

View file

@ -8,7 +8,7 @@ void yargs(hideBin(process.argv))
.command(install)
.command(database)
.demandCommand(1)
.showHelpOnFail(true)
.showHelpOnFail(false)
.strict()
.parserConfiguration({
'dot-notation': false,

View file

@ -0,0 +1,26 @@
import { LogtoConfig, logtoConfigGuards, LogtoConfigKey, LogtoConfigs } from '@logto/schemas';
import { DatabasePool, sql } from 'slonik';
import { z } from 'zod';
import { convertToIdentifiers } from '../database';
const { table, fields } = convertToIdentifiers(LogtoConfigs);
export const getRowsByKeys = async (pool: DatabasePool, keys: LogtoConfigKey[]) =>
pool.query<LogtoConfig>(sql`
select ${sql.join([fields.key, fields.value], sql`,`)} from ${table}
where ${fields.key} in (${sql.join(keys, sql`,`)})
`);
export const updateValueByKey = async <T extends LogtoConfigKey>(
pool: DatabasePool,
key: T,
value: z.infer<typeof logtoConfigGuards[T]>
) =>
pool.query(
sql`
insert into ${table} (${fields.key}, ${fields.value})
values (${key}, ${sql.jsonb(value)})
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
`
);

View file

@ -15,7 +15,7 @@ export const safeExecSync = (command: string) => {
type Log = Readonly<{
info: typeof console.log;
warn: typeof console.log;
error: typeof console.log;
error: (...args: Parameters<typeof console.log>) => never;
}>;
export const log: Log = Object.freeze({
@ -72,3 +72,5 @@ export const downloadFile = async (url: string, destination: string) => {
// Intended
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const noop = () => {};
export const deduplicate = <T>(array: T[]) => [...new Set(array)];

View file

@ -1,4 +1,6 @@
export const alterationStateKey = 'alterationState';
import { LogtoConfigKey } from '@logto/schemas';
export const alterationStateKey: LogtoConfigKey = 'alterationState';
export const logtoConfigsTableFilePath = 'node_modules/@logto/schemas/tables/logto_configs.sql';
export const alterationFilesDirectorySource = 'node_modules/@logto/schemas/alterations';
export const alterationFilesDirectory = 'alterations/';

View file

@ -115,7 +115,7 @@ describe('createLogtoConfigsTable()', () => {
describe('updateDatabaseTimestamp()', () => {
const expectSql = sql`
insert into ${table} (${fields.key}, ${fields.value})
values ($1, $2)
values ($1, $2::jsonb)
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
`;
const updatedAt = '2022-09-21T06:32:46.583Z';

View file

@ -2,12 +2,8 @@ import { existsSync } from 'fs';
import { readdir, readFile } from 'fs/promises';
import path from 'path';
import { LogtoConfig, LogtoConfigs } from '@logto/schemas';
import {
AlterationScript,
AlterationState,
alterationStateGuard,
} from '@logto/schemas/lib/types/alteration';
import { LogtoConfig, LogtoConfigs, AlterationState, alterationStateGuard } from '@logto/schemas';
import { AlterationScript } from '@logto/schemas/lib/types/alteration';
import { conditionalString } from '@silverhand/essentials';
import chalk from 'chalk';
import { copy, remove } from 'fs-extra';
@ -70,7 +66,7 @@ export const updateDatabaseTimestamp = async (pool: DatabasePool, timestamp?: nu
await pool.query(
sql`
insert into ${table} (${fields.key}, ${fields.value})
values (${alterationStateKey}, ${JSON.stringify(value)})
values (${alterationStateKey}, ${sql.jsonb(value)})
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
`
);

View file

@ -1,12 +1,4 @@
import type { DatabaseTransactionConnection } from 'slonik';
import { z } from 'zod';
export const alterationStateGuard = z.object({
timestamp: z.number(),
updatedAt: z.string().optional(),
});
export type AlterationState = z.infer<typeof alterationStateGuard>;
export type AlterationScript = {
up: (connection: DatabaseTransactionConnection) => Promise<void>;

View file

@ -2,3 +2,4 @@ export * from './connector';
export * from './log';
export * from './oidc-config';
export * from './user';
export * from './logto-config';

View file

@ -0,0 +1,30 @@
import { z } from 'zod';
// Alteration state
export const alterationStateGuard = z.object({
timestamp: z.number(),
updatedAt: z.string().optional(),
});
export type AlterationState = z.infer<typeof alterationStateGuard>;
// Logto OIDC config
export const logtoOidcConfigGuard = z.object({
privateKeys: z.string().array().optional(),
cookieKeys: z.string().array().optional(),
refreshTokenReuseInterval: z.number().gte(3).optional(),
});
export type LogtoOidcConfig = z.infer<typeof logtoOidcConfigGuard>;
// Summary
export const logtoConfigGuards = Object.freeze({
alterationState: alterationStateGuard,
oidcConfig: logtoOidcConfigGuard,
} as const);
export type LogtoConfigKey = keyof typeof logtoConfigGuards;
// `as` is intended since we'd like to keep `logtoConfigGuards` as the SSOT of keys
// eslint-disable-next-line no-restricted-syntax
export const logtoConfigKeys = Object.keys(logtoConfigGuards) as LogtoConfigKey[];

View file

@ -20,6 +20,7 @@ importers:
packages/cli:
specifiers:
'@logto/schemas': ^1.0.0-beta.10
'@silverhand/eslint-config': 1.0.0
'@silverhand/ts-config': 1.0.0
'@types/decompress': ^4.2.4
@ -39,12 +40,15 @@ importers:
prettier: ^2.7.1
rimraf: ^3.0.2
semver: ^7.3.7
slonik: ^30.0.0
slonik-interceptor-preset: ^1.2.10
tar: ^6.1.11
ts-node: ^10.9.1
typescript: ^4.7.4
yargs: ^17.6.0
zod: ^3.18.0
dependencies:
'@logto/schemas': link:../schemas
chalk: 4.1.2
find-up: 5.0.0
got: 11.8.3
@ -52,6 +56,8 @@ importers:
inquirer: 8.2.2
ora: 5.4.1
semver: 7.3.7
slonik: 30.1.2
slonik-interceptor-preset: 1.2.10
tar: 6.1.11
yargs: 17.6.0
zod: 3.18.0
@ -1730,6 +1736,7 @@ packages:
pacote: 13.4.1
semver: 7.3.7
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -1760,6 +1767,7 @@ packages:
p-waterfall: 2.1.1
semver: 7.3.7
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -1900,6 +1908,7 @@ packages:
whatwg-url: 8.7.0
yargs-parser: 20.2.4
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2097,6 +2106,7 @@ packages:
npm-registry-fetch: 9.0.0
npmlog: 4.1.2
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2126,6 +2136,7 @@ packages:
pify: 5.0.0
read-package-json: 3.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2164,6 +2175,7 @@ packages:
npmlog: 4.1.2
tar: 6.1.11
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2262,6 +2274,7 @@ packages:
pacote: 13.4.1
semver: 7.3.7
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
dev: true
@ -2307,6 +2320,7 @@ packages:
'@npmcli/run-script': 3.0.2
npmlog: 4.1.2
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2408,6 +2422,7 @@ packages:
slash: 3.0.0
write-json-file: 4.3.0
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
dev: true
@ -2663,6 +2678,7 @@ packages:
treeverse: 2.0.0
walk-up-path: 1.0.0
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2698,6 +2714,8 @@ packages:
promise-retry: 2.0.1
semver: 7.3.7
which: 2.0.2
transitivePeerDependencies:
- bluebird
dev: true
/@npmcli/installed-package-contents/1.0.7:
@ -2728,6 +2746,7 @@ packages:
pacote: 13.4.1
semver: 7.3.7
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -2779,6 +2798,7 @@ packages:
node-gyp: 9.0.0
read-package-json-fast: 2.0.3
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -3831,6 +3851,7 @@ packages:
stylelint-config-xo-scss: 0.15.0_eqpuutlgonckfyjzwkrpusdvaa
transitivePeerDependencies:
- eslint
- eslint-import-resolver-webpack
- postcss
- prettier
- supports-color
@ -3851,6 +3872,7 @@ packages:
stylelint-config-xo-scss: 0.15.0_uyk3cwxn3favstz4untq233szu
transitivePeerDependencies:
- eslint
- eslint-import-resolver-webpack
- postcss
- prettier
- supports-color
@ -5463,6 +5485,8 @@ packages:
ssri: 8.0.1
tar: 6.1.11
unique-filename: 1.1.1
transitivePeerDependencies:
- bluebird
dev: true
/cacache/16.1.0:
@ -5487,6 +5511,8 @@ packages:
ssri: 9.0.1
tar: 6.1.11
unique-filename: 1.1.1
transitivePeerDependencies:
- bluebird
dev: true
/cache-content-type/1.0.1:
@ -6319,6 +6345,18 @@ packages:
ms: 2.1.3
dev: true
/debug/3.2.7_supports-color@5.5.0:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
supports-color: 5.5.0
dev: true
/debug/4.3.3:
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
engines: {node: '>=6.0'}
@ -10126,6 +10164,7 @@ packages:
import-local: 3.1.0
npmlog: 4.1.2
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
dev: true
@ -10160,6 +10199,7 @@ packages:
npm-package-arg: 8.1.5
npm-registry-fetch: 11.0.0
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -10173,6 +10213,7 @@ packages:
semver: 7.3.7
ssri: 8.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -10474,6 +10515,7 @@ packages:
socks-proxy-agent: 6.1.1
ssri: 9.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -10497,6 +10539,7 @@ packages:
socks-proxy-agent: 5.0.1
ssri: 8.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -10521,6 +10564,7 @@ packages:
socks-proxy-agent: 6.1.1
ssri: 8.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -11421,6 +11465,7 @@ packages:
tar: 6.1.11
which: 2.0.2
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -11455,7 +11500,7 @@ packages:
requiresBuild: true
dependencies:
chokidar: 3.5.3
debug: 3.2.7
debug: 3.2.7_supports-color@5.5.0
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
@ -11604,6 +11649,7 @@ packages:
minizlib: 2.1.2
npm-package-arg: 8.1.5
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -11619,6 +11665,7 @@ packages:
npm-package-arg: 9.0.2
proc-log: 2.0.1
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -11635,6 +11682,7 @@ packages:
minizlib: 2.1.2
npm-package-arg: 8.1.5
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -12035,6 +12083,7 @@ packages:
ssri: 9.0.1
tar: 6.1.11
transitivePeerDependencies:
- bluebird
- supports-color
dev: true
@ -12736,6 +12785,11 @@ packages:
/promise-inflight/1.0.1:
resolution: {integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=}
peerDependencies:
bluebird: '*'
peerDependenciesMeta:
bluebird:
optional: true
dev: true
/promise-retry/2.0.1: