@logto/cli 1.0.0-beta.19 → 1.0.0-rc.1
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/lib/commands/database/alteration/index.d.ts +1 -1
- package/lib/commands/database/alteration/index.js +34 -11
- package/lib/commands/database/alteration/index.test.js +5 -5
- package/lib/commands/database/alteration/utils.js +2 -3
- package/lib/commands/database/alteration/version.d.ts +1 -0
- package/lib/commands/database/alteration/version.js +21 -8
- package/lib/commands/database/seed/index.js +33 -10
- package/lib/commands/database/seed/oidc-config.js +1 -7
- package/lib/commands/database/utilities.d.ts +1 -1
- package/lib/commands/database/utilities.js +32 -13
- package/lib/commands/install/utils.js +1 -1
- package/lib/package-json.d.ts +3 -2
- package/lib/package-json.js +13 -9
- package/lib/queries/logto-config.d.ts +6 -9
- package/lib/queries/logto-config.js +3 -26
- package/lib/queries/system.d.ts +4 -0
- package/lib/queries/system.js +43 -0
- package/lib/queries/{logto-config.test.d.ts → system.test.d.ts} +0 -0
- package/lib/queries/{logto-config.test.js → system.test.js} +17 -8
- package/lib/utilities.js +1 -2
- package/package.json +16 -12
- package/lib/commands/database/alteration/meta-url.d.ts +0 -1
- package/lib/commands/database/alteration/meta-url.js +0 -4
- package/lib/meta-url.d.ts +0 -1
- package/lib/meta-url.js +0 -4
|
@@ -2,7 +2,7 @@ import type { DatabasePool } from 'slonik';
|
|
|
2
2
|
import type { CommandModule } from 'yargs';
|
|
3
3
|
import type { AlterationFile } from './type.js';
|
|
4
4
|
export declare const getLatestAlterationTimestamp: () => Promise<number>;
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const getAvailableAlterations: (pool: DatabasePool, compareMode?: 'gt' | 'lte') => Promise<AlterationFile[]>;
|
|
6
6
|
declare const alteration: CommandModule<unknown, {
|
|
7
7
|
action: string;
|
|
8
8
|
target?: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { conditionalString } from '@silverhand/essentials';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { createPoolFromConfig } from '../../../database.js';
|
|
4
|
-
import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/
|
|
4
|
+
import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, } from '../../../queries/system.js';
|
|
5
5
|
import { log } from '../../../utilities.js';
|
|
6
6
|
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
|
|
7
|
-
import { chooseAlterationsByVersion } from './version.js';
|
|
7
|
+
import { chooseAlterationsByVersion, chooseRevertAlterationsByVersion } from './version.js';
|
|
8
8
|
const importAlterationScript = async (filePath) => {
|
|
9
9
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
10
10
|
const module = await import(filePath);
|
|
@@ -19,17 +19,28 @@ export const getLatestAlterationTimestamp = async () => {
|
|
|
19
19
|
}
|
|
20
20
|
return getTimestampFromFilename(lastFile.filename);
|
|
21
21
|
};
|
|
22
|
-
export const
|
|
22
|
+
export const getAvailableAlterations = async (pool, compareMode = 'gt') => {
|
|
23
23
|
const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool);
|
|
24
24
|
const files = await getAlterationFiles();
|
|
25
|
-
return files.filter(({ filename }) =>
|
|
25
|
+
return files.filter(({ filename }) => compareMode === 'gt'
|
|
26
|
+
? getTimestampFromFilename(filename) > databaseTimestamp
|
|
27
|
+
: getTimestampFromFilename(filename) <= databaseTimestamp);
|
|
26
28
|
};
|
|
27
|
-
const deployAlteration = async (pool, { path: filePath, filename }) => {
|
|
28
|
-
const { up } = await importAlterationScript(filePath);
|
|
29
|
+
const deployAlteration = async (pool, { path: filePath, filename }, action = 'up') => {
|
|
30
|
+
const { up, down } = await importAlterationScript(filePath);
|
|
29
31
|
try {
|
|
30
32
|
await pool.transaction(async (connection) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
if (action === 'up') {
|
|
34
|
+
await up(connection);
|
|
35
|
+
await updateDatabaseTimestamp(connection, getTimestampFromFilename(filename));
|
|
36
|
+
}
|
|
37
|
+
if (action === 'down') {
|
|
38
|
+
await down(connection);
|
|
39
|
+
const newTimestamp = getTimestampFromFilename(filename) - 1;
|
|
40
|
+
if (newTimestamp > 0) {
|
|
41
|
+
await updateDatabaseTimestamp(connection, newTimestamp);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
33
44
|
});
|
|
34
45
|
}
|
|
35
46
|
catch (error) {
|
|
@@ -39,14 +50,14 @@ const deployAlteration = async (pool, { path: filePath, filename }) => {
|
|
|
39
50
|
" This alteration didn't change anything since it was in a transaction.\n" +
|
|
40
51
|
' Try to fix the error and deploy again.');
|
|
41
52
|
}
|
|
42
|
-
log.info(`Run alteration ${filename} succeeded`);
|
|
53
|
+
log.info(`Run alteration ${filename} \`${action}()\` function succeeded`);
|
|
43
54
|
};
|
|
44
55
|
const alteration = {
|
|
45
56
|
command: ['alteration <action> [target]', 'alt', 'alter'],
|
|
46
57
|
describe: 'Perform database alteration',
|
|
47
58
|
builder: (yargs) => yargs
|
|
48
59
|
.positional('action', {
|
|
49
|
-
describe: 'The action to perform,
|
|
60
|
+
describe: 'The action to perform, accepts `list`, `deploy`, and `rollback` (or `r`).',
|
|
50
61
|
type: 'string',
|
|
51
62
|
demandOption: true,
|
|
52
63
|
})
|
|
@@ -63,7 +74,7 @@ const alteration = {
|
|
|
63
74
|
}
|
|
64
75
|
else if (action === 'deploy') {
|
|
65
76
|
const pool = await createPoolFromConfig();
|
|
66
|
-
const alterations = await chooseAlterationsByVersion(await
|
|
77
|
+
const alterations = await chooseAlterationsByVersion(await getAvailableAlterations(pool), target);
|
|
67
78
|
log.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to deploy`);
|
|
68
79
|
// The await inside the loop is intended, alterations should run in order
|
|
69
80
|
for (const alteration of alterations) {
|
|
@@ -72,6 +83,18 @@ const alteration = {
|
|
|
72
83
|
}
|
|
73
84
|
await pool.end();
|
|
74
85
|
}
|
|
86
|
+
else if (['rollback', 'r'].includes(action)) {
|
|
87
|
+
const pool = await createPoolFromConfig();
|
|
88
|
+
const alterations = await chooseRevertAlterationsByVersion(await getAvailableAlterations(pool, 'lte'), target ?? '');
|
|
89
|
+
log.info(`Found ${alterations.length} alteration${conditionalString(alterations.length > 1 && 's')} to revert`);
|
|
90
|
+
// The await inside the loop is intended, alterations should run in order
|
|
91
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
92
|
+
for (const alteration of alterations.slice().reverse()) {
|
|
93
|
+
// eslint-disable-next-line no-await-in-loop
|
|
94
|
+
await deployAlteration(pool, alteration, 'down');
|
|
95
|
+
}
|
|
96
|
+
await pool.end();
|
|
97
|
+
}
|
|
75
98
|
else {
|
|
76
99
|
log.error('Unsupported action');
|
|
77
100
|
}
|
|
@@ -15,18 +15,18 @@ const files = Object.freeze([
|
|
|
15
15
|
await mockEsmWithActual('./utils.js', () => ({
|
|
16
16
|
getAlterationFiles: async () => files,
|
|
17
17
|
}));
|
|
18
|
-
const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual('../../../queries/
|
|
18
|
+
const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual('../../../queries/system.js', () => ({
|
|
19
19
|
getCurrentDatabaseAlterationTimestamp: jest.fn(),
|
|
20
20
|
}));
|
|
21
|
-
const {
|
|
22
|
-
describe('
|
|
21
|
+
const { getAvailableAlterations } = await import('./index.js');
|
|
22
|
+
describe('getAvailableAlterations()', () => {
|
|
23
23
|
it('returns all files if database timestamp is 0', async () => {
|
|
24
24
|
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(0);
|
|
25
|
-
await expect(
|
|
25
|
+
await expect(getAvailableAlterations(pool)).resolves.toEqual(files);
|
|
26
26
|
});
|
|
27
27
|
it('returns files whose timestamp is greater then database timestamp', async () => {
|
|
28
28
|
getCurrentDatabaseAlterationTimestamp.mockResolvedValue(1663923770);
|
|
29
|
-
await expect(
|
|
29
|
+
await expect(getAvailableAlterations(pool)).resolves.toEqual([files[1], files[2]]);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
describe('chooseAlterationsByVersion()', () => {
|
|
@@ -4,9 +4,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { findPackage } from '@logto/shared';
|
|
6
6
|
import { getPathInModule } from '../../../utilities.js';
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
|
7
|
+
const currentDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const alterationFilenameRegex = /-([\d.]+)-?.*\.js$/;
|
|
10
9
|
export const getTimestampFromFilename = (filename) => {
|
|
11
10
|
const match = alterationFilenameRegex.exec(filename);
|
|
12
11
|
if (!match?.[1]) {
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { AlterationFile } from './type.js';
|
|
2
2
|
export declare const chooseAlterationsByVersion: (alterations: readonly AlterationFile[], initialVersion?: string) => Promise<AlterationFile[]>;
|
|
3
|
+
export declare const chooseRevertAlterationsByVersion: (alterations: readonly AlterationFile[], version: string) => Promise<AlterationFile[]>;
|
|
@@ -3,12 +3,20 @@ import chalk from 'chalk';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { SemVer, compare, eq, gt } from 'semver';
|
|
5
5
|
import { findLastIndex, isTty, log } from '../../../utilities.js';
|
|
6
|
+
const getVersionStringFromFilename = (filename) => filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown';
|
|
6
7
|
const getVersionFromFilename = (filename) => {
|
|
7
8
|
try {
|
|
8
|
-
return new SemVer(filename
|
|
9
|
+
return new SemVer(getVersionStringFromFilename(filename));
|
|
9
10
|
}
|
|
10
11
|
catch { }
|
|
11
12
|
};
|
|
13
|
+
const getAlterationVersions = (alterations) => alterations
|
|
14
|
+
.map(({ filename }) => getVersionFromFilename(filename))
|
|
15
|
+
.filter((version) => version instanceof SemVer)
|
|
16
|
+
// Cannot use `Set` to deduplicate since it's a class
|
|
17
|
+
.filter((version, index, self) => index === self.findIndex((another) => eq(version, another)))
|
|
18
|
+
.slice()
|
|
19
|
+
.sort((i, j) => compare(j, i));
|
|
12
20
|
const latestTag = 'latest';
|
|
13
21
|
const nextTag = 'next';
|
|
14
22
|
export const chooseAlterationsByVersion = async (alterations, initialVersion) => {
|
|
@@ -20,13 +28,7 @@ export const chooseAlterationsByVersion = async (alterations, initialVersion) =>
|
|
|
20
28
|
log.info(`Deploy target ${chalk.green(nextTag)}`);
|
|
21
29
|
return alterations.slice(0, endIndex + 1);
|
|
22
30
|
}
|
|
23
|
-
const versions = alterations
|
|
24
|
-
.map(({ filename }) => getVersionFromFilename(filename))
|
|
25
|
-
.filter((version) => version instanceof SemVer)
|
|
26
|
-
// Cannot use `Set` to deduplicate since it's a class
|
|
27
|
-
.filter((version, index, self) => index === self.findIndex((another) => eq(version, another)))
|
|
28
|
-
.slice()
|
|
29
|
-
.sort((i, j) => compare(j, i));
|
|
31
|
+
const versions = getAlterationVersions(alterations);
|
|
30
32
|
const initialSemVersion = conditional(initialVersion && initialVersion !== latestTag && new SemVer(initialVersion));
|
|
31
33
|
const firstVersion = versions[0];
|
|
32
34
|
if (!firstVersion) {
|
|
@@ -60,3 +62,14 @@ export const chooseAlterationsByVersion = async (alterations, initialVersion) =>
|
|
|
60
62
|
return version && !gt(version, targetVersion);
|
|
61
63
|
});
|
|
62
64
|
};
|
|
65
|
+
export const chooseRevertAlterationsByVersion = async (alterations, version) => {
|
|
66
|
+
const semVersion = new SemVer(version);
|
|
67
|
+
log.info(`Revert target ${chalk.green(semVersion.version)}`);
|
|
68
|
+
return alterations.filter(({ filename }) => {
|
|
69
|
+
if (getVersionStringFromFilename(filename) === nextTag) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const version = getVersionFromFilename(filename);
|
|
73
|
+
return version && gt(version, semVersion);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { readdir, readFile } from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { generateStandardId } from '@logto/core-kit';
|
|
4
|
-
import { logtoConfigGuards, LogtoOidcConfigKey, managementResource, defaultSignInExperience,
|
|
5
|
-
import { Hooks } from '@logto/schemas/models';
|
|
4
|
+
import { logtoConfigGuards, LogtoOidcConfigKey, managementResource, defaultSignInExperience, createDefaultAdminConsoleConfig, createDemoAppApplication, defaultRole, managementResourceScope, defaultRoleScopeRelation, defaultTenant, } from '@logto/schemas';
|
|
5
|
+
import { Hooks, Tenants } from '@logto/schemas/models';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { sql } from 'slonik';
|
|
8
8
|
import { raw } from 'slonik-sql-tag-raw';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database.js';
|
|
11
|
-
import { getRowsByKeys, doesConfigsTableExist,
|
|
11
|
+
import { getRowsByKeys, doesConfigsTableExist, updateValueByKey, } from '../../../queries/logto-config.js';
|
|
12
|
+
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
|
12
13
|
import { getPathInModule, log, oraPromise } from '../../../utilities.js';
|
|
13
14
|
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
|
14
15
|
import { getAlterationDirectory } from '../alteration/utils.js';
|
|
15
16
|
import { oidcConfigReaders } from './oidc-config.js';
|
|
17
|
+
const getExplicitOrder = (query) => {
|
|
18
|
+
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
|
|
19
|
+
return matched ? Number(matched) : undefined;
|
|
20
|
+
};
|
|
21
|
+
const compareQuery = ([t1, q1], [t2, q2]) => {
|
|
22
|
+
const o1 = getExplicitOrder(q1);
|
|
23
|
+
const o2 = getExplicitOrder(q2);
|
|
24
|
+
if (o1 === undefined && o2 === undefined) {
|
|
25
|
+
return t1.localeCompare(t2);
|
|
26
|
+
}
|
|
27
|
+
if (o1 === undefined) {
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
if (o2 === undefined) {
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
return o1 - o2;
|
|
34
|
+
};
|
|
16
35
|
const createTables = async (connection) => {
|
|
17
36
|
const tableDirectory = getPathInModule('@logto/schemas', 'tables');
|
|
18
37
|
const directoryFiles = await readdir(tableDirectory);
|
|
@@ -21,23 +40,27 @@ const createTables = async (connection) => {
|
|
|
21
40
|
file,
|
|
22
41
|
await readFile(path.join(tableDirectory, file), 'utf8'),
|
|
23
42
|
]));
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
const allQueries = [
|
|
44
|
+
[Hooks.tableName, Hooks.raw],
|
|
45
|
+
[Tenants.tableName, Tenants.raw],
|
|
46
|
+
...queries,
|
|
47
|
+
];
|
|
48
|
+
const sorted = allQueries.slice().sort(compareQuery);
|
|
49
|
+
for (const [, query] of sorted) {
|
|
26
50
|
// eslint-disable-next-line no-await-in-loop
|
|
27
51
|
await connection.query(sql `${raw(query)}`);
|
|
28
52
|
}
|
|
29
|
-
for (const table of [Hooks]) {
|
|
30
|
-
// eslint-disable-next-line no-await-in-loop
|
|
31
|
-
await connection.query(sql `${raw(table.raw)}`);
|
|
32
|
-
}
|
|
33
53
|
};
|
|
34
54
|
const seedTables = async (connection, latestTimestamp) => {
|
|
55
|
+
await connection.query(insertInto(defaultTenant, 'tenants'));
|
|
35
56
|
await Promise.all([
|
|
36
57
|
connection.query(insertInto(managementResource, 'resources')),
|
|
37
|
-
connection.query(insertInto(
|
|
58
|
+
connection.query(insertInto(managementResourceScope, 'scopes')),
|
|
59
|
+
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
|
|
38
60
|
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
|
39
61
|
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
|
|
40
62
|
connection.query(insertInto(defaultRole, 'roles')),
|
|
63
|
+
connection.query(insertInto(defaultRoleScopeRelation, 'roles_scopes')),
|
|
41
64
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
42
65
|
]);
|
|
43
66
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { LogtoOidcConfigKey } from '@logto/schemas';
|
|
3
|
-
import {
|
|
3
|
+
import { getEnvAsStringArray } from '@silverhand/essentials';
|
|
4
4
|
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities.js';
|
|
5
5
|
const isBase64FormatPrivateKey = (key) => !key.includes('-');
|
|
6
6
|
/**
|
|
@@ -51,10 +51,4 @@ export const oidcConfigReaders = {
|
|
|
51
51
|
const keys = getEnvAsStringArray(envKey);
|
|
52
52
|
return { value: keys.length > 0 ? keys : [generateOidcCookieKey()], fromEnv: keys.length > 0 };
|
|
53
53
|
},
|
|
54
|
-
[LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => {
|
|
55
|
-
const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL';
|
|
56
|
-
const raw = Number(getEnv(envKey));
|
|
57
|
-
const value = Math.max(3, raw || 0);
|
|
58
|
-
return { value, fromEnv: raw === value };
|
|
59
|
-
},
|
|
60
54
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const generateOidcPrivateKey: () => Promise<string>;
|
|
1
|
+
export declare const generateOidcPrivateKey: (type?: 'rsa' | 'ec') => Promise<string>;
|
|
2
2
|
export declare const generateOidcCookieKey: () => string;
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { generateKeyPair } from 'crypto';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
-
export const generateOidcPrivateKey = async () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
export const generateOidcPrivateKey = async (type = 'ec') => {
|
|
5
|
+
if (type === 'rsa') {
|
|
6
|
+
const { privateKey } = await promisify(generateKeyPair)('rsa', {
|
|
7
|
+
modulusLength: 4096,
|
|
8
|
+
publicKeyEncoding: {
|
|
9
|
+
type: 'spki',
|
|
10
|
+
format: 'pem',
|
|
11
|
+
},
|
|
12
|
+
privateKeyEncoding: {
|
|
13
|
+
type: 'pkcs8',
|
|
14
|
+
format: 'pem',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return privateKey;
|
|
18
|
+
}
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
20
|
+
if (type === 'ec') {
|
|
21
|
+
const { privateKey } = await promisify(generateKeyPair)('ec', {
|
|
22
|
+
// https://security.stackexchange.com/questions/78621/which-elliptic-curve-should-i-use
|
|
23
|
+
namedCurve: 'secp384r1',
|
|
24
|
+
publicKeyEncoding: {
|
|
25
|
+
type: 'spki',
|
|
26
|
+
format: 'pem',
|
|
27
|
+
},
|
|
28
|
+
privateKeyEncoding: {
|
|
29
|
+
type: 'pkcs8',
|
|
30
|
+
format: 'pem',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return privateKey;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Unsupported private key ${String(type)}`);
|
|
17
36
|
};
|
|
18
37
|
export const generateOidcCookieKey = () => nanoid();
|
|
@@ -15,7 +15,7 @@ import { seedByPool } from '../database/seed/index.js';
|
|
|
15
15
|
export const defaultPath = path.join(os.homedir(), 'logto');
|
|
16
16
|
const pgRequired = new semver.SemVer('14.0.0');
|
|
17
17
|
export const validateNodeVersion = () => {
|
|
18
|
-
const required = [new semver.SemVer('
|
|
18
|
+
const required = [new semver.SemVer('18.12.0')];
|
|
19
19
|
const requiredVersionString = required.map((version) => '^' + version.version).join(' || ');
|
|
20
20
|
const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' }));
|
|
21
21
|
if (required.every((version) => version.major !== current.major)) {
|
package/lib/package-json.d.ts
CHANGED
|
@@ -66,7 +66,6 @@ export declare const packageJson: {
|
|
|
66
66
|
devDependencies: {
|
|
67
67
|
"@silverhand/eslint-config": string;
|
|
68
68
|
"@silverhand/ts-config": string;
|
|
69
|
-
"@types/fs-extra": string;
|
|
70
69
|
"@types/inquirer": string;
|
|
71
70
|
"@types/jest": string;
|
|
72
71
|
"@types/node": string;
|
|
@@ -78,12 +77,14 @@ export declare const packageJson: {
|
|
|
78
77
|
jest: string;
|
|
79
78
|
"lint-staged": string;
|
|
80
79
|
prettier: string;
|
|
81
|
-
rimraf: string;
|
|
82
80
|
sinon: string;
|
|
83
81
|
typescript: string;
|
|
84
82
|
};
|
|
85
83
|
eslintConfig: {
|
|
86
84
|
extends: string;
|
|
85
|
+
rules: {
|
|
86
|
+
complexity: (string | number)[];
|
|
87
|
+
};
|
|
87
88
|
ignorePatterns: string[];
|
|
88
89
|
};
|
|
89
90
|
prettier: string;
|
package/lib/package-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const packageJson = {
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -24,8 +24,8 @@ export const packageJson = {
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"precommit": "lint-staged",
|
|
26
26
|
"prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts",
|
|
27
|
-
"build": "
|
|
28
|
-
"build:test": "
|
|
27
|
+
"build": "rm -rf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json",
|
|
28
|
+
"build:test": "rm -rf lib/ && pnpm prepare:package-json && tsc -p tsconfig.test.json --sourcemap",
|
|
29
29
|
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
|
30
30
|
"start": "node .",
|
|
31
31
|
"start:dev": "pnpm build && node .",
|
|
@@ -37,7 +37,7 @@ export const packageJson = {
|
|
|
37
37
|
"prepack": "pnpm build"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": "^
|
|
40
|
+
"node": "^18.12.0"
|
|
41
41
|
},
|
|
42
42
|
"bugs": {
|
|
43
43
|
"url": "https://github.com/logto-io/logto/issues"
|
|
@@ -52,7 +52,7 @@ export const packageJson = {
|
|
|
52
52
|
"dotenv": "^16.0.0",
|
|
53
53
|
"got": "^12.5.3",
|
|
54
54
|
"hpagent": "^1.2.0",
|
|
55
|
-
"inquirer": "^
|
|
55
|
+
"inquirer": "^9.0.0",
|
|
56
56
|
"nanoid": "^4.0.0",
|
|
57
57
|
"ora": "^6.1.2",
|
|
58
58
|
"p-limit": "^4.0.0",
|
|
@@ -69,10 +69,9 @@ export const packageJson = {
|
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@silverhand/eslint-config": "1.3.0",
|
|
71
71
|
"@silverhand/ts-config": "1.2.1",
|
|
72
|
-
"@types/
|
|
73
|
-
"@types/inquirer": "^8.2.1",
|
|
72
|
+
"@types/inquirer": "^9.0.0",
|
|
74
73
|
"@types/jest": "^29.1.2",
|
|
75
|
-
"@types/node": "^
|
|
74
|
+
"@types/node": "^18.11.18",
|
|
76
75
|
"@types/semver": "^7.3.12",
|
|
77
76
|
"@types/sinon": "^10.0.13",
|
|
78
77
|
"@types/tar": "^6.1.2",
|
|
@@ -81,12 +80,17 @@ export const packageJson = {
|
|
|
81
80
|
"jest": "^29.3.1",
|
|
82
81
|
"lint-staged": "^13.0.0",
|
|
83
82
|
"prettier": "^2.8.1",
|
|
84
|
-
"rimraf": "^3.0.2",
|
|
85
83
|
"sinon": "^15.0.0",
|
|
86
84
|
"typescript": "^4.9.4"
|
|
87
85
|
},
|
|
88
86
|
"eslintConfig": {
|
|
89
87
|
"extends": "@silverhand",
|
|
88
|
+
"rules": {
|
|
89
|
+
"complexity": [
|
|
90
|
+
"error",
|
|
91
|
+
11
|
|
92
|
+
]
|
|
93
|
+
},
|
|
90
94
|
"ignorePatterns": [
|
|
91
95
|
"src/package-json.ts"
|
|
92
96
|
]
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { LogtoConfig, LogtoConfigKey } from '@logto/schemas';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const updateValueByKey: <T extends LogtoConfigKey>(pool: DatabasePool | DatabaseTransactionConnection, key: T, value: z.TypeOf<import("@logto/schemas").LogtoConfigGuard[T]>) => Promise<import("slonik").QueryResult<import("slonik").QueryResultRow>>;
|
|
8
|
-
export declare const getCurrentDatabaseAlterationTimestamp: (pool: DatabasePool) => Promise<number>;
|
|
9
|
-
export declare const updateDatabaseTimestamp: (connection: DatabaseTransactionConnection, timestamp: number) => Promise<import("slonik").QueryResult<import("slonik").QueryResultRow>>;
|
|
1
|
+
import type { LogtoConfig, LogtoConfigKey, logtoConfigGuards } from '@logto/schemas';
|
|
2
|
+
import type { CommonQueryMethods } from 'slonik';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
export declare const doesConfigsTableExist: (pool: CommonQueryMethods) => Promise<boolean>;
|
|
5
|
+
export declare const getRowsByKeys: (pool: CommonQueryMethods, keys: LogtoConfigKey[]) => Promise<import("slonik").QueryResult<LogtoConfig>>;
|
|
6
|
+
export declare const updateValueByKey: <T extends LogtoConfigKey>(pool: CommonQueryMethods, key: T, value: z.TypeOf<import("@logto/schemas").LogtoConfigGuard[T]>) => Promise<import("slonik").QueryResult<import("slonik").QueryResultRow>>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LogtoConfigs } from '@logto/schemas';
|
|
2
2
|
import { convertToIdentifiers } from '@logto/shared';
|
|
3
3
|
import { sql } from 'slonik';
|
|
4
|
-
import { z } from 'zod';
|
|
5
4
|
const { table, fields } = convertToIdentifiers(LogtoConfigs);
|
|
6
5
|
export const doesConfigsTableExist = async (pool) => {
|
|
7
6
|
const { rows } = await pool.query(sql `select to_regclass(${LogtoConfigs.table}) as regclass`);
|
|
@@ -14,28 +13,6 @@ export const getRowsByKeys = async (pool, keys) => pool.query(sql `
|
|
|
14
13
|
export const updateValueByKey = async (pool, key, value) => pool.query(sql `
|
|
15
14
|
insert into ${table} (${fields.key}, ${fields.value})
|
|
16
15
|
values (${key}, ${sql.jsonb(value)})
|
|
17
|
-
on conflict (${fields.
|
|
16
|
+
on conflict (${fields.tenantId}, ${fields.key})
|
|
17
|
+
do update set ${fields.value}=excluded.${fields.value}
|
|
18
18
|
`);
|
|
19
|
-
export const getCurrentDatabaseAlterationTimestamp = async (pool) => {
|
|
20
|
-
try {
|
|
21
|
-
const result = await pool.maybeOne(sql `select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}`);
|
|
22
|
-
const parsed = logtoConfigGuards[AlterationStateKey.AlterationState].safeParse(result?.value);
|
|
23
|
-
return (parsed.success && parsed.data.timestamp) || 0;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
const result = z.object({ code: z.string() }).safeParse(error);
|
|
27
|
-
// Relation does not exist, treat as 0
|
|
28
|
-
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
29
|
-
if (result.success && result.data.code === '42P01') {
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
export const updateDatabaseTimestamp = async (connection, timestamp) => {
|
|
36
|
-
const value = {
|
|
37
|
-
timestamp,
|
|
38
|
-
updatedAt: new Date().toISOString(),
|
|
39
|
-
};
|
|
40
|
-
return updateValueByKey(connection, AlterationStateKey.AlterationState, value);
|
|
41
|
-
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik';
|
|
2
|
+
export declare const doesSystemsTableExist: (pool: CommonQueryMethods) => Promise<boolean>;
|
|
3
|
+
export declare const getCurrentDatabaseAlterationTimestamp: (pool: CommonQueryMethods) => Promise<number>;
|
|
4
|
+
export declare const updateDatabaseTimestamp: (connection: DatabaseTransactionConnection, timestamp: number) => Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { systemGuards, Systems, AlterationStateKey } from '@logto/schemas';
|
|
2
|
+
import { convertToIdentifiers } from '@logto/shared';
|
|
3
|
+
import { sql } from 'slonik';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const { fields } = convertToIdentifiers(Systems);
|
|
6
|
+
const doesTableExist = async (pool, table) => {
|
|
7
|
+
const { rows } = await pool.query(sql `select to_regclass(${table}) as regclass`);
|
|
8
|
+
return Boolean(rows[0]?.regclass);
|
|
9
|
+
};
|
|
10
|
+
export const doesSystemsTableExist = async (pool) => doesTableExist(pool, Systems.table);
|
|
11
|
+
const legacyLogtoConfigsTable = '_logto_configs';
|
|
12
|
+
const getAlterationStateTable = async (pool) => (await doesSystemsTableExist(pool))
|
|
13
|
+
? sql.identifier([Systems.table])
|
|
14
|
+
: sql.identifier([legacyLogtoConfigsTable]); // Fall back to the old config table
|
|
15
|
+
export const getCurrentDatabaseAlterationTimestamp = async (pool) => {
|
|
16
|
+
const table = await getAlterationStateTable(pool);
|
|
17
|
+
try {
|
|
18
|
+
const result = await pool.maybeOne(sql `select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}`);
|
|
19
|
+
const parsed = systemGuards[AlterationStateKey.AlterationState].safeParse(result?.value);
|
|
20
|
+
return (parsed.success && parsed.data.timestamp) || 0;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const result = z.object({ code: z.string() }).safeParse(error);
|
|
24
|
+
// Relation does not exist, treat as 0
|
|
25
|
+
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
26
|
+
if (result.success && result.data.code === '42P01') {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export const updateDatabaseTimestamp = async (connection, timestamp) => {
|
|
33
|
+
const table = await getAlterationStateTable(connection);
|
|
34
|
+
const value = {
|
|
35
|
+
timestamp,
|
|
36
|
+
updatedAt: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
await connection.query(sql `
|
|
39
|
+
insert into ${table} (${fields.key}, ${fields.value})
|
|
40
|
+
values (${AlterationStateKey.AlterationState}, ${sql.jsonb(value)})
|
|
41
|
+
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
|
42
|
+
`);
|
|
43
|
+
};
|
|
File without changes
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { AlterationStateKey,
|
|
1
|
+
import { AlterationStateKey, Systems } from '@logto/schemas';
|
|
2
2
|
import { convertToIdentifiers } from '@logto/shared';
|
|
3
3
|
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
|
4
4
|
import { expectSqlAssert } from '../test-utilities.js';
|
|
5
|
-
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './
|
|
5
|
+
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './system.js';
|
|
6
6
|
const { jest } = import.meta;
|
|
7
7
|
const mockQuery = jest.fn();
|
|
8
8
|
const pool = createMockPool({
|
|
@@ -10,18 +10,21 @@ const pool = createMockPool({
|
|
|
10
10
|
return mockQuery(sql, values);
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
|
-
const { table, fields } = convertToIdentifiers(
|
|
13
|
+
const { table, fields } = convertToIdentifiers(Systems);
|
|
14
14
|
const timestamp = 1663923776;
|
|
15
|
+
const systemsTableExists = async () => createMockQueryResult([{ regclass: true }]);
|
|
15
16
|
describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
|
16
17
|
it('returns 0 if query failed (table not found)', async () => {
|
|
17
|
-
mockQuery.mockRejectedValueOnce({ code: '42P01' });
|
|
18
|
+
mockQuery.mockImplementationOnce(systemsTableExists).mockRejectedValueOnce({ code: '42P01' });
|
|
18
19
|
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0);
|
|
19
20
|
});
|
|
20
21
|
it('returns 0 if the row is not found', async () => {
|
|
21
22
|
const expectSql = sql `
|
|
22
23
|
select * from ${table} where ${fields.key}=$1
|
|
23
24
|
`;
|
|
24
|
-
mockQuery
|
|
25
|
+
mockQuery
|
|
26
|
+
.mockImplementationOnce(systemsTableExists)
|
|
27
|
+
.mockImplementationOnce(async (sql, values) => {
|
|
25
28
|
expectSqlAssert(sql, expectSql.sql);
|
|
26
29
|
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
|
27
30
|
return createMockQueryResult([]);
|
|
@@ -32,7 +35,9 @@ describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
|
|
32
35
|
const expectSql = sql `
|
|
33
36
|
select * from ${table} where ${fields.key}=$1
|
|
34
37
|
`;
|
|
35
|
-
mockQuery
|
|
38
|
+
mockQuery
|
|
39
|
+
.mockImplementationOnce(systemsTableExists)
|
|
40
|
+
.mockImplementationOnce(async (sql, values) => {
|
|
36
41
|
expectSqlAssert(sql, expectSql.sql);
|
|
37
42
|
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
|
38
43
|
return createMockQueryResult([{ value: 'some_value' }]);
|
|
@@ -43,7 +48,9 @@ describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
|
|
43
48
|
const expectSql = sql `
|
|
44
49
|
select * from ${table} where ${fields.key}=$1
|
|
45
50
|
`;
|
|
46
|
-
mockQuery
|
|
51
|
+
mockQuery
|
|
52
|
+
.mockImplementationOnce(systemsTableExists)
|
|
53
|
+
.mockImplementationOnce(async (sql, values) => {
|
|
47
54
|
expectSqlAssert(sql, expectSql.sql);
|
|
48
55
|
expect(values).toEqual([AlterationStateKey.AlterationState]);
|
|
49
56
|
// @ts-expect-error createMockQueryResult doesn't support jsonb
|
|
@@ -67,7 +74,9 @@ describe('updateDatabaseTimestamp()', () => {
|
|
|
67
74
|
jest.useRealTimers();
|
|
68
75
|
});
|
|
69
76
|
it('sends upsert sql with timestamp and updatedAt', async () => {
|
|
70
|
-
mockQuery
|
|
77
|
+
mockQuery
|
|
78
|
+
.mockImplementationOnce(systemsTableExists)
|
|
79
|
+
.mockImplementationOnce(async (sql, values) => {
|
|
71
80
|
expectSqlAssert(sql, expectSql.sql);
|
|
72
81
|
expect(values).toEqual([
|
|
73
82
|
AlterationStateKey.AlterationState,
|
package/lib/utilities.js
CHANGED
|
@@ -10,7 +10,6 @@ import { HttpsProxyAgent } from 'hpagent';
|
|
|
10
10
|
import inquirer from 'inquirer';
|
|
11
11
|
import ora from 'ora';
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
-
import { metaUrl } from './meta-url.js';
|
|
14
13
|
export const safeExecSync = (command) => {
|
|
15
14
|
try {
|
|
16
15
|
return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
|
|
@@ -66,7 +65,7 @@ export const downloadFile = async (url, destination) => {
|
|
|
66
65
|
};
|
|
67
66
|
export const getPathInModule = (moduleName, relativePath = '/') =>
|
|
68
67
|
// https://stackoverflow.com/a/49455609/12514940
|
|
69
|
-
path.join(path.dirname(createRequire(
|
|
68
|
+
path.join(path.dirname(createRequire(import.meta.url).resolve(`${moduleName}/package.json`)), relativePath);
|
|
70
69
|
export const oraPromise = async (promise, options, exitOnError = false) => {
|
|
71
70
|
const spinner = ora(options).start();
|
|
72
71
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"url": "git+https://github.com/logto-io/logto.git"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": "^
|
|
25
|
+
"node": "^18.12.0"
|
|
26
26
|
},
|
|
27
27
|
"bugs": {
|
|
28
28
|
"url": "https://github.com/logto-io/logto/issues"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dotenv": "^16.0.0",
|
|
35
35
|
"got": "^12.5.3",
|
|
36
36
|
"hpagent": "^1.2.0",
|
|
37
|
-
"inquirer": "^
|
|
37
|
+
"inquirer": "^9.0.0",
|
|
38
38
|
"nanoid": "^4.0.0",
|
|
39
39
|
"ora": "^6.1.2",
|
|
40
40
|
"p-limit": "^4.0.0",
|
|
@@ -47,17 +47,16 @@
|
|
|
47
47
|
"tar": "^6.1.11",
|
|
48
48
|
"yargs": "^17.6.0",
|
|
49
49
|
"zod": "^3.20.2",
|
|
50
|
-
"@logto/core-kit": "1.0.0-
|
|
51
|
-
"@logto/schemas": "1.0.0-
|
|
52
|
-
"@logto/shared": "1.0.0-
|
|
50
|
+
"@logto/core-kit": "1.0.0-rc.0",
|
|
51
|
+
"@logto/schemas": "1.0.0-rc.0",
|
|
52
|
+
"@logto/shared": "1.0.0-rc.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@silverhand/eslint-config": "1.3.0",
|
|
56
56
|
"@silverhand/ts-config": "1.2.1",
|
|
57
|
-
"@types/
|
|
58
|
-
"@types/inquirer": "^8.2.1",
|
|
57
|
+
"@types/inquirer": "^9.0.0",
|
|
59
58
|
"@types/jest": "^29.1.2",
|
|
60
|
-
"@types/node": "^
|
|
59
|
+
"@types/node": "^18.11.18",
|
|
61
60
|
"@types/semver": "^7.3.12",
|
|
62
61
|
"@types/sinon": "^10.0.13",
|
|
63
62
|
"@types/tar": "^6.1.2",
|
|
@@ -66,12 +65,17 @@
|
|
|
66
65
|
"jest": "^29.3.1",
|
|
67
66
|
"lint-staged": "^13.0.0",
|
|
68
67
|
"prettier": "^2.8.1",
|
|
69
|
-
"rimraf": "^3.0.2",
|
|
70
68
|
"sinon": "^15.0.0",
|
|
71
69
|
"typescript": "^4.9.4"
|
|
72
70
|
},
|
|
73
71
|
"eslintConfig": {
|
|
74
72
|
"extends": "@silverhand",
|
|
73
|
+
"rules": {
|
|
74
|
+
"complexity": [
|
|
75
|
+
"error",
|
|
76
|
+
11
|
|
77
|
+
]
|
|
78
|
+
},
|
|
75
79
|
"ignorePatterns": [
|
|
76
80
|
"src/package-json.ts"
|
|
77
81
|
]
|
|
@@ -80,8 +84,8 @@
|
|
|
80
84
|
"scripts": {
|
|
81
85
|
"precommit": "lint-staged",
|
|
82
86
|
"prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts",
|
|
83
|
-
"build": "
|
|
84
|
-
"build:test": "
|
|
87
|
+
"build": "rm -rf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json",
|
|
88
|
+
"build:test": "rm -rf lib/ && pnpm prepare:package-json && tsc -p tsconfig.test.json --sourcemap",
|
|
85
89
|
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
|
|
86
90
|
"start": "node .",
|
|
87
91
|
"start:dev": "pnpm build && node .",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const metaUrl: string;
|
package/lib/meta-url.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const metaUrl: string;
|
package/lib/meta-url.js
DELETED