@logto/cli 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/bin/logto +2 -0
- package/lib/commands/connector/add.d.ts +9 -0
- package/lib/commands/connector/add.js +36 -0
- package/lib/commands/connector/index.d.ts +3 -0
- package/lib/commands/connector/index.js +25 -0
- package/lib/commands/connector/list.d.ts +7 -0
- package/lib/commands/connector/list.js +28 -0
- package/lib/commands/connector/remove.d.ts +8 -0
- package/lib/commands/connector/remove.js +47 -0
- package/lib/commands/connector/utils.d.ts +24 -0
- package/lib/commands/connector/utils.js +165 -0
- package/lib/commands/database/alteration/index.d.ts +11 -0
- package/lib/commands/database/alteration/index.js +142 -0
- package/lib/commands/database/alteration/index.test.d.ts +1 -0
- package/lib/commands/database/alteration/index.test.js +87 -0
- package/lib/commands/database/alteration/type.d.ts +4 -0
- package/lib/commands/database/alteration/type.js +2 -0
- package/lib/commands/database/alteration/version.d.ts +2 -0
- package/lib/commands/database/alteration/version.js +69 -0
- package/lib/commands/database/config.d.ts +3 -0
- package/lib/commands/database/config.js +82 -0
- package/lib/commands/database/index.d.ts +3 -0
- package/lib/commands/database/index.js +16 -0
- package/lib/commands/database/seed/index.d.ts +10 -0
- package/lib/commands/database/seed/index.js +138 -0
- package/lib/commands/database/seed/oidc-config.d.ts +12 -0
- package/lib/commands/database/seed/oidc-config.js +77 -0
- package/lib/commands/install/index.d.ts +14 -0
- package/lib/commands/install/index.js +71 -0
- package/lib/commands/install/utils.d.ts +11 -0
- package/lib/commands/install/utils.js +176 -0
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +4 -0
- package/lib/database.d.ts +12 -0
- package/lib/database.js +71 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +56 -0
- package/lib/package-json.d.ts +87 -0
- package/lib/package-json.js +95 -0
- package/lib/queries/logto-config.d.ts +8 -0
- package/lib/queries/logto-config.js +49 -0
- package/lib/queries/logto-config.test.d.ts +1 -0
- package/lib/queries/logto-config.test.js +81 -0
- package/lib/test-utilities.d.ts +7 -0
- package/lib/test-utilities.js +17 -0
- package/lib/utilities.d.ts +35 -0
- package/lib/utilities.js +136 -0
- package/package.json +92 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addOfficialConnectors = exports.addConnectors = exports.getConnectorPackagesFrom = exports.isOfficialConnector = exports.normalizePackageName = exports.inquireInstancePath = exports.npmPackResultGuard = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const util_1 = require("util");
|
|
12
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
13
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
14
|
+
const fs_extra_1 = require("fs-extra");
|
|
15
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
16
|
+
const p_retry_1 = __importDefault(require("p-retry"));
|
|
17
|
+
const tar_1 = __importDefault(require("tar"));
|
|
18
|
+
const zod_1 = require("zod");
|
|
19
|
+
const constants_1 = require("../../constants");
|
|
20
|
+
const utilities_1 = require("../../utilities");
|
|
21
|
+
const utils_1 = require("../install/utils");
|
|
22
|
+
const coreDirectory = 'packages/core';
|
|
23
|
+
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
24
|
+
exports.npmPackResultGuard = zod_1.z
|
|
25
|
+
.object({
|
|
26
|
+
name: zod_1.z.string(),
|
|
27
|
+
version: zod_1.z.string(),
|
|
28
|
+
filename: zod_1.z.string(),
|
|
29
|
+
})
|
|
30
|
+
.array();
|
|
31
|
+
const buildPathErrorMessage = (value) => `The path ${chalk_1.default.green(value)} does not contain a Logto instance, please try another.`;
|
|
32
|
+
const validatePath = async (value) => {
|
|
33
|
+
const corePackageJsonPath = path_1.default.resolve(path_1.default.join(value, coreDirectory, 'package.json'));
|
|
34
|
+
if (!(0, fs_1.existsSync)(corePackageJsonPath)) {
|
|
35
|
+
return buildPathErrorMessage(value);
|
|
36
|
+
}
|
|
37
|
+
const packageJson = await (0, promises_1.readFile)(corePackageJsonPath, { encoding: 'utf8' });
|
|
38
|
+
const packageName = await zod_1.z
|
|
39
|
+
.object({ name: zod_1.z.string() })
|
|
40
|
+
.parseAsync(JSON.parse(packageJson))
|
|
41
|
+
.then(({ name }) => name)
|
|
42
|
+
.catch(() => '');
|
|
43
|
+
if (packageName !== '@logto/core') {
|
|
44
|
+
return buildPathErrorMessage(value);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
const inquireInstancePath = async (initialPath) => {
|
|
49
|
+
const inquire = async () => {
|
|
50
|
+
if (!(0, utilities_1.isTty)()) {
|
|
51
|
+
(0, essentials_1.assert)(initialPath, new Error('Path is missing'));
|
|
52
|
+
return initialPath;
|
|
53
|
+
}
|
|
54
|
+
const { instancePath } = await inquirer_1.default.prompt({
|
|
55
|
+
name: 'instancePath',
|
|
56
|
+
message: 'Where is your Logto instance?',
|
|
57
|
+
type: 'input',
|
|
58
|
+
default: utils_1.defaultPath,
|
|
59
|
+
filter: (value) => value.trim(),
|
|
60
|
+
validate: validatePath,
|
|
61
|
+
}, { instancePath: initialPath });
|
|
62
|
+
return instancePath;
|
|
63
|
+
};
|
|
64
|
+
const instancePath = await inquire();
|
|
65
|
+
const validated = await validatePath(instancePath);
|
|
66
|
+
if (validated !== true) {
|
|
67
|
+
utilities_1.log.error(validated);
|
|
68
|
+
}
|
|
69
|
+
return instancePath;
|
|
70
|
+
};
|
|
71
|
+
exports.inquireInstancePath = inquireInstancePath;
|
|
72
|
+
const packagePrefix = 'connector-';
|
|
73
|
+
const normalizePackageName = (name) => name
|
|
74
|
+
.split('/')
|
|
75
|
+
// Prepend prefix to the last fragment if needed
|
|
76
|
+
.map((fragment, index, array) => index === array.length - 1 && !fragment.startsWith(packagePrefix) && !fragment.startsWith('@')
|
|
77
|
+
? packagePrefix + fragment
|
|
78
|
+
: fragment)
|
|
79
|
+
.join('/');
|
|
80
|
+
exports.normalizePackageName = normalizePackageName;
|
|
81
|
+
const getConnectorDirectory = (instancePath) => path_1.default.join(instancePath, coreDirectory, constants_1.connectorDirectory);
|
|
82
|
+
const isOfficialConnector = (packageName) => packageName.startsWith('@logto/connector-');
|
|
83
|
+
exports.isOfficialConnector = isOfficialConnector;
|
|
84
|
+
const getConnectorPackageName = async (directory) => {
|
|
85
|
+
const filePath = path_1.default.join(directory, 'package.json');
|
|
86
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const json = await (0, promises_1.readFile)(filePath, 'utf8');
|
|
90
|
+
const { name } = zod_1.z.object({ name: zod_1.z.string() }).parse(JSON.parse(json));
|
|
91
|
+
if (name.startsWith('connector-') || Boolean(name.split('/')[1]?.startsWith('connector-'))) {
|
|
92
|
+
return name;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const getConnectorPackagesFrom = async (instancePath) => {
|
|
96
|
+
const directory = getConnectorDirectory(await (0, exports.inquireInstancePath)(instancePath));
|
|
97
|
+
const content = await (0, promises_1.readdir)(directory, 'utf8');
|
|
98
|
+
const rawPackages = await Promise.all(content.map(async (value) => {
|
|
99
|
+
const currentDirectory = path_1.default.join(directory, value);
|
|
100
|
+
return { name: await getConnectorPackageName(currentDirectory), path: currentDirectory };
|
|
101
|
+
}));
|
|
102
|
+
return rawPackages.filter((packageInfo) => typeof packageInfo.name === 'string');
|
|
103
|
+
};
|
|
104
|
+
exports.getConnectorPackagesFrom = getConnectorPackagesFrom;
|
|
105
|
+
const addConnectors = async (instancePath, packageNames) => {
|
|
106
|
+
const cwd = getConnectorDirectory(instancePath);
|
|
107
|
+
if (!(0, fs_1.existsSync)(cwd)) {
|
|
108
|
+
await (0, promises_1.mkdir)(cwd);
|
|
109
|
+
}
|
|
110
|
+
utilities_1.log.info('Fetch connector metadata');
|
|
111
|
+
const results = await Promise.all(packageNames
|
|
112
|
+
.map((name) => (0, exports.normalizePackageName)(name))
|
|
113
|
+
.map(async (packageName) => {
|
|
114
|
+
const run = async () => {
|
|
115
|
+
const { stdout } = await execPromise(`npm pack ${packageName} --json`, { cwd });
|
|
116
|
+
const result = exports.npmPackResultGuard.parse(JSON.parse(stdout));
|
|
117
|
+
if (!result[0]) {
|
|
118
|
+
throw new Error(`Unable to execute ${chalk_1.default.green('npm pack')} on package ${chalk_1.default.green(packageName)}`);
|
|
119
|
+
}
|
|
120
|
+
const { filename, name } = result[0];
|
|
121
|
+
const escapedFilename = filename.replace(/\//g, '-').replace(/@/g, '');
|
|
122
|
+
const tarPath = path_1.default.join(cwd, escapedFilename);
|
|
123
|
+
const packageDirectory = path_1.default.join(cwd, name.replace(/\//g, '-'));
|
|
124
|
+
await (0, fs_extra_1.remove)(packageDirectory);
|
|
125
|
+
await (0, fs_extra_1.ensureDir)(packageDirectory);
|
|
126
|
+
await tar_1.default.extract({ cwd: packageDirectory, file: tarPath, strip: 1 });
|
|
127
|
+
await (0, promises_1.unlink)(tarPath);
|
|
128
|
+
utilities_1.log.succeed(`Added ${chalk_1.default.green(name)}`);
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await (0, p_retry_1.default)(run, { retries: 2 });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.warn(`[${packageName}]`, error);
|
|
135
|
+
return packageName;
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
const errorPackages = results.filter(Boolean);
|
|
139
|
+
const errorCount = errorPackages.length;
|
|
140
|
+
utilities_1.log.info(errorCount
|
|
141
|
+
? `Finished with ${errorCount} error${(0, essentials_1.conditionalString)(errorCount > 1 && 's')}.`
|
|
142
|
+
: 'Finished');
|
|
143
|
+
if (errorCount) {
|
|
144
|
+
utilities_1.log.warn('Failed to add ' + errorPackages.map((name) => chalk_1.default.green(name)).join(', '));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
exports.addConnectors = addConnectors;
|
|
148
|
+
const officialConnectorPrefix = '@logto/connector-';
|
|
149
|
+
const fetchOfficialConnectorList = async () => {
|
|
150
|
+
const { stdout } = await execPromise(`npm search ${officialConnectorPrefix} --json`);
|
|
151
|
+
const packages = zod_1.z
|
|
152
|
+
.object({ name: zod_1.z.string() })
|
|
153
|
+
.transform(({ name }) => name)
|
|
154
|
+
.array()
|
|
155
|
+
.parse(JSON.parse(stdout));
|
|
156
|
+
return packages.filter((name) => ['mock', 'kit'].every((excluded) => !name.slice(officialConnectorPrefix.length).startsWith(excluded)));
|
|
157
|
+
};
|
|
158
|
+
const addOfficialConnectors = async (instancePath) => {
|
|
159
|
+
const packages = await (0, utilities_1.oraPromise)(fetchOfficialConnectorList(), {
|
|
160
|
+
text: 'Fetch official connector list',
|
|
161
|
+
prefixText: chalk_1.default.blue('[info]'),
|
|
162
|
+
});
|
|
163
|
+
await (0, exports.addConnectors)(instancePath, packages);
|
|
164
|
+
};
|
|
165
|
+
exports.addOfficialConnectors = addOfficialConnectors;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DatabasePool } from 'slonik';
|
|
2
|
+
import { CommandModule } from 'yargs';
|
|
3
|
+
import { AlterationFile } from './type';
|
|
4
|
+
export declare const getAlterationFiles: () => Promise<AlterationFile[]>;
|
|
5
|
+
export declare const getLatestAlterationTimestamp: () => Promise<number>;
|
|
6
|
+
export declare const getUndeployedAlterations: (pool: DatabasePool) => Promise<AlterationFile[]>;
|
|
7
|
+
declare const alteration: CommandModule<unknown, {
|
|
8
|
+
action: string;
|
|
9
|
+
target?: string;
|
|
10
|
+
}>;
|
|
11
|
+
export default alteration;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.getUndeployedAlterations = exports.getLatestAlterationTimestamp = exports.getAlterationFiles = void 0;
|
|
30
|
+
const path_1 = __importDefault(require("path"));
|
|
31
|
+
const shared_1 = require("@logto/shared");
|
|
32
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
33
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
34
|
+
const fs_extra_1 = require("fs-extra");
|
|
35
|
+
const database_1 = require("../../../database");
|
|
36
|
+
const logto_config_1 = require("../../../queries/logto-config");
|
|
37
|
+
const utilities_1 = require("../../../utilities");
|
|
38
|
+
const version_1 = require("./version");
|
|
39
|
+
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
|
40
|
+
const getTimestampFromFilename = (filename) => {
|
|
41
|
+
const match = alterationFilenameRegex.exec(filename);
|
|
42
|
+
if (!match?.[1]) {
|
|
43
|
+
throw new Error(`Can not get timestamp: ${filename}`);
|
|
44
|
+
}
|
|
45
|
+
return Number(match[1]);
|
|
46
|
+
};
|
|
47
|
+
const importAlterationScript = async (filePath) => {
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
49
|
+
const module = await Promise.resolve().then(() => __importStar(require(filePath)));
|
|
50
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
51
|
+
return module.default;
|
|
52
|
+
};
|
|
53
|
+
const getAlterationFiles = async () => {
|
|
54
|
+
const alterationDirectory = (0, utilities_1.getPathInModule)('@logto/schemas', 'alterations-js');
|
|
55
|
+
/**
|
|
56
|
+
* We copy all alteration scripts to the CLI package root directory,
|
|
57
|
+
* since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`.
|
|
58
|
+
* While the original `@logto/schemas` may remove them in production.
|
|
59
|
+
*/
|
|
60
|
+
const packageDirectory = await (0, shared_1.findPackage)(
|
|
61
|
+
// Until we migrate to ESM
|
|
62
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
63
|
+
__dirname);
|
|
64
|
+
const localAlterationDirectory = path_1.default.resolve(
|
|
65
|
+
// Until we migrate to ESM
|
|
66
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
67
|
+
packageDirectory ?? __dirname, 'alteration-scripts');
|
|
68
|
+
if (!(0, fs_extra_1.existsSync)(alterationDirectory)) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
// We need to copy alteration files to execute in the CLI context to make `slonik` available
|
|
72
|
+
await (0, fs_extra_1.remove)(localAlterationDirectory);
|
|
73
|
+
await (0, fs_extra_1.copy)(alterationDirectory, localAlterationDirectory);
|
|
74
|
+
const directory = await (0, fs_extra_1.readdir)(localAlterationDirectory);
|
|
75
|
+
const files = directory.filter((file) => alterationFilenameRegex.test(file));
|
|
76
|
+
return files
|
|
77
|
+
.slice()
|
|
78
|
+
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
|
79
|
+
.map((filename) => ({ path: path_1.default.join(localAlterationDirectory, filename), filename }));
|
|
80
|
+
};
|
|
81
|
+
exports.getAlterationFiles = getAlterationFiles;
|
|
82
|
+
const getLatestAlterationTimestamp = async () => {
|
|
83
|
+
const files = await (0, exports.getAlterationFiles)();
|
|
84
|
+
const lastFile = files[files.length - 1];
|
|
85
|
+
if (!lastFile) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
return getTimestampFromFilename(lastFile.filename);
|
|
89
|
+
};
|
|
90
|
+
exports.getLatestAlterationTimestamp = getLatestAlterationTimestamp;
|
|
91
|
+
const getUndeployedAlterations = async (pool) => {
|
|
92
|
+
const databaseTimestamp = await (0, logto_config_1.getCurrentDatabaseAlterationTimestamp)(pool);
|
|
93
|
+
const files = await (0, exports.getAlterationFiles)();
|
|
94
|
+
return files.filter(({ filename }) => getTimestampFromFilename(filename) > databaseTimestamp);
|
|
95
|
+
};
|
|
96
|
+
exports.getUndeployedAlterations = getUndeployedAlterations;
|
|
97
|
+
const deployAlteration = async (pool, { path: filePath, filename }) => {
|
|
98
|
+
const { up } = await importAlterationScript(filePath);
|
|
99
|
+
try {
|
|
100
|
+
await pool.transaction(async (connection) => {
|
|
101
|
+
await up(connection);
|
|
102
|
+
await (0, logto_config_1.updateDatabaseTimestamp)(connection, getTimestampFromFilename(filename));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error(error);
|
|
107
|
+
await pool.end();
|
|
108
|
+
utilities_1.log.error(`Error ocurred during running alteration ${chalk_1.default.blue(filename)}.\n\n` +
|
|
109
|
+
" This alteration didn't change anything since it was in a transaction.\n" +
|
|
110
|
+
' Try to fix the error and deploy again.');
|
|
111
|
+
}
|
|
112
|
+
utilities_1.log.info(`Run alteration ${filename} succeeded`);
|
|
113
|
+
};
|
|
114
|
+
const alteration = {
|
|
115
|
+
command: ['alteration <action> [target]', 'alt', 'alter'],
|
|
116
|
+
describe: 'Perform database alteration',
|
|
117
|
+
builder: (yargs) => yargs
|
|
118
|
+
.positional('action', {
|
|
119
|
+
describe: 'The action to perform, now it only accepts `deploy`',
|
|
120
|
+
type: 'string',
|
|
121
|
+
demandOption: true,
|
|
122
|
+
})
|
|
123
|
+
.positional('target', {
|
|
124
|
+
describe: 'The target Logto version for alteration',
|
|
125
|
+
type: 'string',
|
|
126
|
+
}),
|
|
127
|
+
handler: async ({ action, target }) => {
|
|
128
|
+
if (action !== 'deploy') {
|
|
129
|
+
utilities_1.log.error('Unsupported action');
|
|
130
|
+
}
|
|
131
|
+
const pool = await (0, database_1.createPoolFromConfig)();
|
|
132
|
+
const alterations = await (0, version_1.chooseAlterationsByVersion)(await (0, exports.getUndeployedAlterations)(pool), target);
|
|
133
|
+
utilities_1.log.info(`Found ${alterations.length} alteration${(0, essentials_1.conditionalString)(alterations.length > 1 && 's')} to deploy`);
|
|
134
|
+
// The await inside the loop is intended, alterations should run in order
|
|
135
|
+
for (const alteration of alterations) {
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
137
|
+
await deployAlteration(pool, alteration);
|
|
138
|
+
}
|
|
139
|
+
await pool.end();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
exports.default = alteration;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const slonik_1 = require("slonik");
|
|
27
|
+
const functions = __importStar(require("."));
|
|
28
|
+
const queries = __importStar(require("../../../queries/logto-config"));
|
|
29
|
+
const version_1 = require("./version");
|
|
30
|
+
const mockQuery = jest.fn();
|
|
31
|
+
const pool = (0, slonik_1.createMockPool)({
|
|
32
|
+
query: async (sql, values) => {
|
|
33
|
+
return mockQuery(sql, values);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
describe('getUndeployedAlterations()', () => {
|
|
37
|
+
const files = Object.freeze([
|
|
38
|
+
{ filename: '1.0.0-1663923770-a.js', path: '/alterations-js/1.0.0-1663923770-a.js' },
|
|
39
|
+
{ filename: '1.0.0-1663923771-b.js', path: '/alterations-js/1.0.0-1663923771-b.js' },
|
|
40
|
+
{ filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' },
|
|
41
|
+
]);
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// `getAlterationFiles()` will ensure the order
|
|
44
|
+
jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]);
|
|
45
|
+
});
|
|
46
|
+
it('returns all files if database timestamp is 0', async () => {
|
|
47
|
+
jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0);
|
|
48
|
+
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files);
|
|
49
|
+
});
|
|
50
|
+
it('returns files whose timestamp is greater then database timestamp', async () => {
|
|
51
|
+
jest
|
|
52
|
+
.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp')
|
|
53
|
+
.mockResolvedValueOnce(1663923770);
|
|
54
|
+
await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('chooseAlterationsByVersion()', () => {
|
|
58
|
+
const files = Object.freeze([
|
|
59
|
+
'1.0.0_beta.9-1663923770-a.js',
|
|
60
|
+
'1.0.0_beta.9-1663923771-b.js',
|
|
61
|
+
'1.0.0_beta.10-1663923772-c.js',
|
|
62
|
+
'1.0.0_beta.11-1663923773-c.js',
|
|
63
|
+
'1.0.0_beta.11-1663923774-c.js',
|
|
64
|
+
'1.0.0-1663923775-c.js',
|
|
65
|
+
'1.0.0-1663923776-c.js',
|
|
66
|
+
'1.0.1-1663923777-c.js',
|
|
67
|
+
'1.2.0-1663923778-c.js',
|
|
68
|
+
'next-1663923778-c.js',
|
|
69
|
+
'next-1663923779-c.js',
|
|
70
|
+
'next-1663923780-c.js',
|
|
71
|
+
'next1-1663923781-c.js',
|
|
72
|
+
].map((filename) => ({ filename, path: '/alterations/' + filename })));
|
|
73
|
+
it('chooses nothing when input version is invalid', async () => {
|
|
74
|
+
await expect((0, version_1.chooseAlterationsByVersion)(files, 'next1')).rejects.toThrow('Invalid Version: next1');
|
|
75
|
+
await expect((0, version_1.chooseAlterationsByVersion)([], 'ok')).rejects.toThrow('Invalid Version: ok');
|
|
76
|
+
});
|
|
77
|
+
it('chooses correct alteration files', async () => {
|
|
78
|
+
await Promise.all([
|
|
79
|
+
expect((0, version_1.chooseAlterationsByVersion)([], 'v1.0.0')).resolves.toEqual([]),
|
|
80
|
+
expect((0, version_1.chooseAlterationsByVersion)(files, 'v1.0.0')).resolves.toEqual(files.slice(0, 7)),
|
|
81
|
+
expect((0, version_1.chooseAlterationsByVersion)(files, 'v1.0.0-beta.10')).resolves.toEqual(files.slice(0, 3)),
|
|
82
|
+
expect((0, version_1.chooseAlterationsByVersion)(files, 'v1.1.0')).resolves.toEqual(files.slice(0, 8)),
|
|
83
|
+
expect((0, version_1.chooseAlterationsByVersion)(files, 'v1.2.0')).resolves.toEqual(files.slice(0, 9)),
|
|
84
|
+
expect((0, version_1.chooseAlterationsByVersion)(files, 'next')).resolves.toEqual(files.slice(0, 12)),
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.chooseAlterationsByVersion = void 0;
|
|
7
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const semver_1 = require("semver");
|
|
11
|
+
const utilities_1 = require("../../../utilities");
|
|
12
|
+
const getVersionFromFilename = (filename) => {
|
|
13
|
+
try {
|
|
14
|
+
return new semver_1.SemVer(filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown');
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
};
|
|
18
|
+
const latestTag = 'latest';
|
|
19
|
+
const nextTag = 'next';
|
|
20
|
+
const chooseAlterationsByVersion = async (alterations, initialVersion) => {
|
|
21
|
+
if (initialVersion === nextTag) {
|
|
22
|
+
const endIndex = (0, utilities_1.findLastIndex)(alterations, ({ filename }) => filename.startsWith(nextTag + '-') || Boolean(getVersionFromFilename(filename)));
|
|
23
|
+
if (endIndex === -1) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
utilities_1.log.info(`Deploy target ${chalk_1.default.green(nextTag)}`);
|
|
27
|
+
return alterations.slice(0, endIndex + 1);
|
|
28
|
+
}
|
|
29
|
+
const versions = alterations
|
|
30
|
+
.map(({ filename }) => getVersionFromFilename(filename))
|
|
31
|
+
.filter((version) => version instanceof semver_1.SemVer)
|
|
32
|
+
// Cannot use `Set` to deduplicate since it's a class
|
|
33
|
+
.filter((version, index, self) => index === self.findIndex((another) => (0, semver_1.eq)(version, another)))
|
|
34
|
+
.slice()
|
|
35
|
+
.sort((i, j) => (0, semver_1.compare)(j, i));
|
|
36
|
+
const initialSemVersion = (0, essentials_1.conditional)(initialVersion && initialVersion !== latestTag && new semver_1.SemVer(initialVersion));
|
|
37
|
+
const firstVersion = versions[0];
|
|
38
|
+
if (!firstVersion) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const getTargetVersion = async () => {
|
|
42
|
+
if (initialVersion === latestTag) {
|
|
43
|
+
return firstVersion;
|
|
44
|
+
}
|
|
45
|
+
if (!(0, utilities_1.isTty)()) {
|
|
46
|
+
(0, essentials_1.assert)(initialSemVersion, new Error('Missing target version'));
|
|
47
|
+
return initialSemVersion;
|
|
48
|
+
}
|
|
49
|
+
const { version } = await inquirer_1.default.prompt({
|
|
50
|
+
type: 'list',
|
|
51
|
+
message: 'Choose the alteration target version',
|
|
52
|
+
name: 'version',
|
|
53
|
+
choices: versions.map((semVersion) => ({
|
|
54
|
+
name: semVersion.version,
|
|
55
|
+
value: semVersion,
|
|
56
|
+
})),
|
|
57
|
+
}, {
|
|
58
|
+
version: initialSemVersion,
|
|
59
|
+
});
|
|
60
|
+
return version;
|
|
61
|
+
};
|
|
62
|
+
const targetVersion = await getTargetVersion();
|
|
63
|
+
utilities_1.log.info(`Deploy target ${chalk_1.default.green(targetVersion.version)}`);
|
|
64
|
+
return alterations.filter(({ filename }) => {
|
|
65
|
+
const version = getVersionFromFilename(filename);
|
|
66
|
+
return version && !(0, semver_1.gt)(version, targetVersion);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
exports.chooseAlterationsByVersion = chooseAlterationsByVersion;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const schemas_1 = require("@logto/schemas");
|
|
7
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const database_1 = require("../../database");
|
|
10
|
+
const logto_config_1 = require("../../queries/logto-config");
|
|
11
|
+
const utilities_1 = require("../../utilities");
|
|
12
|
+
const validKeysDisplay = chalk_1.default.green(schemas_1.logtoConfigKeys.join(', '));
|
|
13
|
+
const validateKeys = (keys) => {
|
|
14
|
+
const invalidKey = (Array.isArray(keys) ? keys : [keys]).find(
|
|
15
|
+
// Using `.includes()` will result a type error
|
|
16
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
17
|
+
(key) => !schemas_1.logtoConfigKeys.some((element) => element === key));
|
|
18
|
+
if (invalidKey) {
|
|
19
|
+
utilities_1.log.error(`Invalid config key ${chalk_1.default.red(invalidKey)} found, expected one of ${validKeysDisplay}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const getConfig = {
|
|
23
|
+
command: 'get <key> [keys...]',
|
|
24
|
+
describe: 'Get config value(s) of the given key(s) in Logto database',
|
|
25
|
+
builder: (yargs) => yargs
|
|
26
|
+
.positional('key', {
|
|
27
|
+
describe: `The key to get from database, one of ${validKeysDisplay}`,
|
|
28
|
+
type: 'string',
|
|
29
|
+
demandOption: true,
|
|
30
|
+
})
|
|
31
|
+
.positional('keys', {
|
|
32
|
+
describe: 'The additional keys to get from database',
|
|
33
|
+
type: 'string',
|
|
34
|
+
array: true,
|
|
35
|
+
default: [],
|
|
36
|
+
}),
|
|
37
|
+
handler: async ({ key, keys }) => {
|
|
38
|
+
const queryKeys = (0, essentials_1.deduplicate)([key, ...keys]);
|
|
39
|
+
validateKeys(queryKeys);
|
|
40
|
+
const pool = await (0, database_1.createPoolFromConfig)();
|
|
41
|
+
const { rows } = await (0, logto_config_1.getRowsByKeys)(pool, queryKeys);
|
|
42
|
+
await pool.end();
|
|
43
|
+
console.log(queryKeys
|
|
44
|
+
.map((currentKey) => {
|
|
45
|
+
const value = rows.find(({ key }) => currentKey === key)?.value;
|
|
46
|
+
return (chalk_1.default.magenta(currentKey) +
|
|
47
|
+
'=' +
|
|
48
|
+
(value === undefined ? chalk_1.default.gray(value) : chalk_1.default.green(JSON.stringify(value))));
|
|
49
|
+
})
|
|
50
|
+
.join('\n'));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const setConfig = {
|
|
54
|
+
command: 'set <key> <value>',
|
|
55
|
+
describe: 'Set config value of the given key in Logto database',
|
|
56
|
+
builder: (yargs) => yargs
|
|
57
|
+
.positional('key', {
|
|
58
|
+
describe: `The key to get from database, one of ${validKeysDisplay}`,
|
|
59
|
+
type: 'string',
|
|
60
|
+
demandOption: true,
|
|
61
|
+
})
|
|
62
|
+
.positional('value', {
|
|
63
|
+
describe: 'The value to set, should be a valid JSON string',
|
|
64
|
+
type: 'string',
|
|
65
|
+
demandOption: true,
|
|
66
|
+
}),
|
|
67
|
+
handler: async ({ key, value }) => {
|
|
68
|
+
validateKeys(key);
|
|
69
|
+
const guarded = schemas_1.logtoConfigGuards[key].parse(JSON.parse(value));
|
|
70
|
+
const pool = await (0, database_1.createPoolFromConfig)();
|
|
71
|
+
await (0, logto_config_1.updateValueByKey)(pool, key, guarded);
|
|
72
|
+
await pool.end();
|
|
73
|
+
utilities_1.log.info(`Update ${chalk_1.default.green(key)} succeeded`);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const config = {
|
|
77
|
+
command: ['config', 'configs'],
|
|
78
|
+
describe: 'Commands for Logto database config',
|
|
79
|
+
builder: (yargs) => yargs.command(getConfig).command(setConfig).demandCommand(1),
|
|
80
|
+
handler: essentials_1.noop,
|
|
81
|
+
};
|
|
82
|
+
exports.default = config;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const essentials_1 = require("@silverhand/essentials");
|
|
7
|
+
const alteration_1 = __importDefault(require("./alteration"));
|
|
8
|
+
const config_1 = __importDefault(require("./config"));
|
|
9
|
+
const seed_1 = __importDefault(require("./seed"));
|
|
10
|
+
const database = {
|
|
11
|
+
command: ['database', 'db'],
|
|
12
|
+
describe: 'Commands for Logto database',
|
|
13
|
+
builder: (yargs) => yargs.command(config_1.default).command(seed_1.default).command(alteration_1.default).demandCommand(1),
|
|
14
|
+
handler: essentials_1.noop,
|
|
15
|
+
};
|
|
16
|
+
exports.default = database;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DatabasePool } from 'slonik';
|
|
2
|
+
import { CommandModule } from 'yargs';
|
|
3
|
+
declare const seedChoices: readonly ["all", "oidc"];
|
|
4
|
+
declare type SeedChoice = typeof seedChoices[number];
|
|
5
|
+
export declare const seedByPool: (pool: DatabasePool, type: SeedChoice) => Promise<void>;
|
|
6
|
+
declare const seed: CommandModule<Record<string, unknown>, {
|
|
7
|
+
type: string;
|
|
8
|
+
swe?: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export default seed;
|