@logto/cli 1.0.0-rc.2 → 1.0.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/LICENSE +6 -0
- package/lib/commands/connector/add.d.ts +1 -0
- package/lib/commands/connector/add.js +12 -2
- package/lib/commands/connector/utils.d.ts +1 -1
- package/lib/commands/connector/utils.js +5 -4
- package/lib/commands/database/index.js +2 -1
- package/lib/commands/database/seed/cloud.d.ts +10 -0
- package/lib/commands/database/seed/cloud.js +34 -0
- package/lib/commands/database/seed/index.d.ts +2 -4
- package/lib/commands/database/seed/index.js +21 -28
- package/lib/commands/database/seed/tables.d.ts +2 -1
- package/lib/commands/database/seed/tables.js +27 -7
- package/lib/commands/database/seed/tenant.d.ts +3 -2
- package/lib/commands/database/seed/tenant.js +25 -3
- package/lib/commands/database/system.d.ts +3 -0
- package/lib/commands/database/system.js +63 -0
- package/lib/commands/install/index.d.ts +2 -2
- package/lib/commands/install/index.js +8 -7
- package/lib/commands/install/utils.d.ts +1 -1
- package/lib/commands/install/utils.js +2 -2
- package/lib/connector/consts.d.ts +7 -0
- package/lib/connector/consts.js +10 -0
- package/lib/connector/factories.d.ts +2 -0
- package/lib/connector/factories.js +34 -0
- package/lib/connector/index.d.ts +4 -0
- package/lib/connector/index.js +4 -0
- package/lib/connector/loader.d.ts +2 -0
- package/lib/connector/loader.js +48 -0
- package/lib/connector/types.d.ts +12 -0
- package/lib/connector/types.js +1 -0
- package/lib/connector/utils.d.ts +11 -0
- package/lib/connector/utils.js +51 -0
- package/lib/database.js +2 -3
- package/lib/package-json.d.ts +2 -0
- package/lib/package-json.js +6 -4
- package/lib/queries/system.d.ts +5 -0
- package/lib/queries/system.js +13 -4
- package/lib/queries/system.test.js +5 -1
- package/package.json +9 -7
- package/LICENSE.cloud +0 -93
package/LICENSE
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
Portions of this software are licensed as follows:
|
|
2
|
+
|
|
3
|
+
* All content that resides under the "packages/cloud" directory of this repository, if that directory exists, is licensed under the license defined in "packages/cloud/LICENSE" (Elastic-2.0).
|
|
4
|
+
* All third party components incorporated into this software are licensed under the original license provided by the owner of the applicable component.
|
|
5
|
+
* Content outside of the above mentioned directories or restrictions above is available under the "MPL-2.0" license as defined below.
|
|
6
|
+
|
|
1
7
|
Mozilla Public License Version 2.0
|
|
2
8
|
==================================
|
|
3
9
|
|
|
@@ -16,11 +16,21 @@ const add = {
|
|
|
16
16
|
default: false,
|
|
17
17
|
describe: 'Add all official connectors.\n' +
|
|
18
18
|
"If it's true, the specified package names will be ignored.",
|
|
19
|
+
})
|
|
20
|
+
.option('cloud', {
|
|
21
|
+
describe: 'Add additional connectors for Logto Cloud',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
default: false,
|
|
24
|
+
hidden: true,
|
|
19
25
|
}),
|
|
20
|
-
handler: async ({ packages: packageNames, path, official }) => {
|
|
26
|
+
handler: async ({ packages: packageNames, path, official, cloud }) => {
|
|
21
27
|
const instancePath = await inquireInstancePath(path);
|
|
28
|
+
if (cloud && !official) {
|
|
29
|
+
log.error('--cloud option can only be used with --official option');
|
|
30
|
+
}
|
|
22
31
|
if (official) {
|
|
23
|
-
|
|
32
|
+
// Add demo connectors for Logto Cloud only
|
|
33
|
+
await addOfficialConnectors(instancePath, cloud);
|
|
24
34
|
}
|
|
25
35
|
else {
|
|
26
36
|
if (!packageNames?.length) {
|
|
@@ -17,4 +17,4 @@ export declare const normalizePackageName: (name: string) => string;
|
|
|
17
17
|
export declare const isOfficialConnector: (packageName: string) => boolean;
|
|
18
18
|
export declare const getConnectorPackagesFrom: (instancePath?: string) => Promise<import("../../utils.js").ConnectorPackage[]>;
|
|
19
19
|
export declare const addConnectors: (instancePath: string, packageNames: string[]) => Promise<void>;
|
|
20
|
-
export declare const addOfficialConnectors: (instancePath: string) => Promise<void>;
|
|
20
|
+
export declare const addOfficialConnectors: (instancePath: string, includingCloudConnectors?: boolean) => Promise<void>;
|
|
@@ -127,7 +127,7 @@ export const addConnectors = async (instancePath, packageNames) => {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
const officialConnectorPrefix = '@logto/connector-';
|
|
130
|
-
const fetchOfficialConnectorList = async () => {
|
|
130
|
+
const fetchOfficialConnectorList = async (includingCloudConnectors = false) => {
|
|
131
131
|
const fetchList = async (from = 0, size = 20) => {
|
|
132
132
|
const parameters = new URLSearchParams({
|
|
133
133
|
text: officialConnectorPrefix,
|
|
@@ -142,10 +142,11 @@ const fetchOfficialConnectorList = async () => {
|
|
|
142
142
|
for (let page = 0;; ++page) {
|
|
143
143
|
// eslint-disable-next-line no-await-in-loop
|
|
144
144
|
const { objects } = await fetchList(page * 20, 20);
|
|
145
|
+
const excludeList = ['mock', 'kit', ...(includingCloudConnectors ? [] : ['logto'])];
|
|
145
146
|
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
146
147
|
packages.push(...objects
|
|
147
148
|
.filter(({ package: { name, scope } }) => scope === 'logto' &&
|
|
148
|
-
|
|
149
|
+
excludeList.every((excluded) => !name.slice(officialConnectorPrefix.length).startsWith(excluded)))
|
|
149
150
|
.map(({ package: data }) => data));
|
|
150
151
|
if (objects.length < 20) {
|
|
151
152
|
break;
|
|
@@ -153,8 +154,8 @@ const fetchOfficialConnectorList = async () => {
|
|
|
153
154
|
}
|
|
154
155
|
return packages;
|
|
155
156
|
};
|
|
156
|
-
export const addOfficialConnectors = async (instancePath) => {
|
|
157
|
-
const packages = await oraPromise(fetchOfficialConnectorList(), {
|
|
157
|
+
export const addOfficialConnectors = async (instancePath, includingCloudConnectors = false) => {
|
|
158
|
+
const packages = await oraPromise(fetchOfficialConnectorList(includingCloudConnectors), {
|
|
158
159
|
text: 'Fetch official connector list',
|
|
159
160
|
prefixText: chalk.blue('[info]'),
|
|
160
161
|
});
|
|
@@ -2,10 +2,11 @@ import { noop } from '@silverhand/essentials';
|
|
|
2
2
|
import alteration from './alteration/index.js';
|
|
3
3
|
import config from './config.js';
|
|
4
4
|
import seed from './seed/index.js';
|
|
5
|
+
import system from './system.js';
|
|
5
6
|
const database = {
|
|
6
7
|
command: ['database', 'db'],
|
|
7
8
|
describe: 'Commands for Logto database',
|
|
8
|
-
builder: (yargs) => yargs.command(config).command(seed).command(alteration).demandCommand(1),
|
|
9
|
+
builder: (yargs) => yargs.command(config).command(seed).command(alteration).command(system).demandCommand(1),
|
|
9
10
|
handler: noop,
|
|
10
11
|
};
|
|
11
12
|
export default database;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommonQueryMethods } from 'slonik';
|
|
2
|
+
/**
|
|
3
|
+
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
4
|
+
* It reads the same env variables as core to construct the cloud `UrlSet`.
|
|
5
|
+
*
|
|
6
|
+
* E.g., by default, it will appends `http://localhost:3003/default/callback` to the Redirect URIs.
|
|
7
|
+
*
|
|
8
|
+
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
9
|
+
*/
|
|
10
|
+
export declare const appendAdminConsoleRedirectUris: (pool: CommonQueryMethods) => Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { adminConsoleApplicationId, adminTenantId, defaultTenantId } from '@logto/schemas';
|
|
2
|
+
import { GlobalValues } from '@logto/shared';
|
|
3
|
+
import { appendPath } from '@silverhand/essentials';
|
|
4
|
+
import { sql } from 'slonik';
|
|
5
|
+
import { log } from '../../../utils.js';
|
|
6
|
+
/**
|
|
7
|
+
* Append Redirect URIs for the default tenant callback in cloud Admin Console.
|
|
8
|
+
* It reads the same env variables as core to construct the cloud `UrlSet`.
|
|
9
|
+
*
|
|
10
|
+
* E.g., by default, it will appends `http://localhost:3003/default/callback` to the Redirect URIs.
|
|
11
|
+
*
|
|
12
|
+
* For why it is necessary, see the redirect lifecycle of cloud Admin Console.
|
|
13
|
+
*/
|
|
14
|
+
export const appendAdminConsoleRedirectUris = async (pool) => {
|
|
15
|
+
const redirectUris = new GlobalValues().cloudUrlSet
|
|
16
|
+
.deduplicated()
|
|
17
|
+
.flatMap((endpoint) => [defaultTenantId, adminTenantId].map((tenantId) => appendPath(endpoint, tenantId, 'callback')));
|
|
18
|
+
const metadataKey = sql.identifier(['oidc_client_metadata']);
|
|
19
|
+
// Copied from packages/cloud/src/queries/tenants.ts
|
|
20
|
+
// Can be merged into the original once we remove slonik
|
|
21
|
+
await pool.query(sql `
|
|
22
|
+
update applications
|
|
23
|
+
set ${metadataKey} = jsonb_set(
|
|
24
|
+
${metadataKey},
|
|
25
|
+
'{redirectUris}',
|
|
26
|
+
(select jsonb_agg(distinct value) from jsonb_array_elements(
|
|
27
|
+
${metadataKey}->'redirectUris' || ${sql.jsonb(redirectUris.map(String))}
|
|
28
|
+
))
|
|
29
|
+
)
|
|
30
|
+
where id = ${adminConsoleApplicationId}
|
|
31
|
+
and tenant_id = ${adminTenantId}
|
|
32
|
+
`);
|
|
33
|
+
log.succeed('Appended initial Redirect URIs to Admin Console:', redirectUris.map(String));
|
|
34
|
+
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { DatabasePool } from 'slonik';
|
|
2
2
|
import type { CommandModule } from 'yargs';
|
|
3
|
-
declare const
|
|
4
|
-
type SeedChoice = (typeof seedChoices)[number];
|
|
5
|
-
export declare const seedByPool: (pool: DatabasePool, type: SeedChoice) => Promise<void>;
|
|
3
|
+
export declare const seedByPool: (pool: DatabasePool, cloud?: boolean) => Promise<void>;
|
|
6
4
|
declare const seed: CommandModule<Record<string, unknown>, {
|
|
7
|
-
type: string;
|
|
8
5
|
swe?: boolean;
|
|
6
|
+
cloud?: boolean;
|
|
9
7
|
}>;
|
|
10
8
|
export default seed;
|
|
@@ -4,25 +4,22 @@ import { doesConfigsTableExist } from '../../../queries/logto-config.js';
|
|
|
4
4
|
import { log, oraPromise } from '../../../utils.js';
|
|
5
5
|
import { getLatestAlterationTimestamp } from '../alteration/index.js';
|
|
6
6
|
import { getAlterationDirectory } from '../alteration/utils.js';
|
|
7
|
-
import { createTables, seedTables } from './tables.js';
|
|
8
|
-
const
|
|
9
|
-
export const seedByPool = async (pool, type) => {
|
|
7
|
+
import { createTables, seedCloud, seedTables } from './tables.js';
|
|
8
|
+
export const seedByPool = async (pool, cloud = false) => {
|
|
10
9
|
await pool.transaction(async (connection) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
prefixText: chalk.blue('[info]'),
|
|
25
|
-
});
|
|
10
|
+
// Check alteration scripts available in order to insert correct timestamp
|
|
11
|
+
const latestTimestamp = await getLatestAlterationTimestamp();
|
|
12
|
+
if (latestTimestamp < 1) {
|
|
13
|
+
throw new Error(`No alteration script found when seeding the database.\n` +
|
|
14
|
+
`Please check \`${getAlterationDirectory()}\` to see if there are alteration scripts available.\n`);
|
|
15
|
+
}
|
|
16
|
+
await oraPromise(createTables(connection), {
|
|
17
|
+
text: 'Create tables',
|
|
18
|
+
prefixText: chalk.blue('[info]'),
|
|
19
|
+
});
|
|
20
|
+
await seedTables(connection, latestTimestamp, cloud);
|
|
21
|
+
if (cloud) {
|
|
22
|
+
await seedCloud(connection);
|
|
26
23
|
}
|
|
27
24
|
});
|
|
28
25
|
};
|
|
@@ -35,13 +32,12 @@ const seed = {
|
|
|
35
32
|
alias: 'skip-when-exists',
|
|
36
33
|
type: 'boolean',
|
|
37
34
|
})
|
|
38
|
-
.
|
|
39
|
-
describe: '
|
|
40
|
-
type: '
|
|
41
|
-
|
|
42
|
-
default: 'all',
|
|
35
|
+
.option('cloud', {
|
|
36
|
+
describe: 'Seed additional cloud data',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
hidden: true,
|
|
43
39
|
}),
|
|
44
|
-
handler: async ({
|
|
40
|
+
handler: async ({ swe, cloud }) => {
|
|
45
41
|
const pool = await createPoolAndDatabaseIfNeeded();
|
|
46
42
|
if (swe && (await doesConfigsTableExist(pool))) {
|
|
47
43
|
log.info('Seeding skipped');
|
|
@@ -49,10 +45,7 @@ const seed = {
|
|
|
49
45
|
return;
|
|
50
46
|
}
|
|
51
47
|
try {
|
|
52
|
-
|
|
53
|
-
// The value of `type` can be ensured, so it's safe to use `as` here.
|
|
54
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
55
|
-
await seedByPool(pool, type);
|
|
48
|
+
await seedByPool(pool, cloud);
|
|
56
49
|
}
|
|
57
50
|
catch (error) {
|
|
58
51
|
console.error(error);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { DatabaseTransactionConnection } from 'slonik';
|
|
2
2
|
export declare const createTables: (connection: DatabaseTransactionConnection) => Promise<void>;
|
|
3
|
-
export declare const seedTables: (connection: DatabaseTransactionConnection, latestTimestamp: number) => Promise<void>;
|
|
3
|
+
export declare const seedTables: (connection: DatabaseTransactionConnection, latestTimestamp: number, isCloud: boolean) => Promise<void>;
|
|
4
|
+
export declare const seedCloud: (connection: DatabaseTransactionConnection) => Promise<void>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readdir, readFile } from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, } from '@logto/schemas';
|
|
4
|
-
import {
|
|
3
|
+
import { createDefaultAdminConsoleConfig, defaultTenantId, adminTenantId, defaultManagementApi, createAdminDataInAdminTenant, createMeApiInAdminTenant, createDefaultSignInExperience, createAdminTenantSignInExperience, createDefaultAdminConsoleApplication, createCloudApi, createTenantApplicationRole, createTenantMachineToMachineApplication, createAdminTenantApplicationRole, CloudScope, } from '@logto/schemas';
|
|
4
|
+
import { Tenants } from '@logto/schemas/models';
|
|
5
5
|
import { sql } from 'slonik';
|
|
6
6
|
import { raw } from 'slonik-sql-tag-raw';
|
|
7
7
|
import { insertInto } from '../../../database.js';
|
|
8
8
|
import { getDatabaseName } from '../../../queries/database.js';
|
|
9
9
|
import { updateDatabaseTimestamp } from '../../../queries/system.js';
|
|
10
|
-
import { getPathInModule } from '../../../utils.js';
|
|
10
|
+
import { getPathInModule, log } from '../../../utils.js';
|
|
11
|
+
import { appendAdminConsoleRedirectUris } from './cloud.js';
|
|
11
12
|
import { seedOidcConfigs } from './oidc-config.js';
|
|
12
|
-
import { createTenant, seedAdminData } from './tenant.js';
|
|
13
|
+
import { assignScopesToRole, createTenant, seedAdminData } from './tenant.js';
|
|
13
14
|
const getExplicitOrder = (query) => {
|
|
14
15
|
const matched = /\/\*\s*init_order\s*=\s*([\d.]+)\s*\*\//.exec(query)?.[1];
|
|
15
16
|
return matched ? Number(matched) : undefined;
|
|
@@ -54,7 +55,6 @@ export const createTables = async (connection) => {
|
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
const allQueries = [
|
|
57
|
-
[Hooks.tableName, Hooks.raw],
|
|
58
58
|
[Tenants.tableName, Tenants.raw],
|
|
59
59
|
...queries.filter(([file]) => !lifecycleNames.includes(file.slice(1, -4))),
|
|
60
60
|
];
|
|
@@ -71,19 +71,39 @@ export const createTables = async (connection) => {
|
|
|
71
71
|
/* eslint-enable no-await-in-loop */
|
|
72
72
|
await runLifecycleQuery('after_all', { database });
|
|
73
73
|
};
|
|
74
|
-
export const seedTables = async (connection, latestTimestamp) => {
|
|
74
|
+
export const seedTables = async (connection, latestTimestamp, isCloud) => {
|
|
75
75
|
await createTenant(connection, defaultTenantId);
|
|
76
76
|
await seedOidcConfigs(connection, defaultTenantId);
|
|
77
77
|
await seedAdminData(connection, defaultManagementApi);
|
|
78
78
|
await createTenant(connection, adminTenantId);
|
|
79
79
|
await seedOidcConfigs(connection, adminTenantId);
|
|
80
80
|
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
|
|
81
|
+
const adminAdminData = createAdminDataInAdminTenant(adminTenantId);
|
|
82
|
+
await seedAdminData(connection, adminAdminData);
|
|
81
83
|
await seedAdminData(connection, createMeApiInAdminTenant());
|
|
84
|
+
const [cloudData, ...cloudAdditionalScopes] = createCloudApi();
|
|
85
|
+
const applicationRole = createTenantApplicationRole();
|
|
86
|
+
await seedAdminData(connection, cloudData, ...cloudAdditionalScopes);
|
|
87
|
+
await connection.query(insertInto(applicationRole, 'roles'));
|
|
88
|
+
await assignScopesToRole(connection, adminTenantId, applicationRole.id, ...cloudAdditionalScopes
|
|
89
|
+
.filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail)
|
|
90
|
+
.map(({ id }) => id));
|
|
91
|
+
// Add M2M app for default tenant
|
|
92
|
+
const defaultTenantApplication = createTenantMachineToMachineApplication(defaultTenantId);
|
|
93
|
+
await connection.query(insertInto(defaultTenantApplication, 'applications'));
|
|
94
|
+
await connection.query(insertInto(createAdminTenantApplicationRole(defaultTenantApplication.id, applicationRole.id), 'applications_roles'));
|
|
95
|
+
// Assign all cloud API scopes to role `admin:admin`
|
|
96
|
+
await assignScopesToRole(connection, adminTenantId, adminAdminData.role.id, ...cloudAdditionalScopes.map(({ id }) => id));
|
|
82
97
|
await Promise.all([
|
|
83
98
|
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
|
84
|
-
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId), 'sign_in_experiences')),
|
|
99
|
+
connection.query(insertInto(createDefaultSignInExperience(defaultTenantId, isCloud), 'sign_in_experiences')),
|
|
100
|
+
connection.query(insertInto(createDefaultAdminConsoleConfig(adminTenantId), 'logto_configs')),
|
|
85
101
|
connection.query(insertInto(createAdminTenantSignInExperience(), 'sign_in_experiences')),
|
|
86
102
|
connection.query(insertInto(createDefaultAdminConsoleApplication(), 'applications')),
|
|
87
103
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
|
88
104
|
]);
|
|
105
|
+
log.succeed('Seed data');
|
|
106
|
+
};
|
|
107
|
+
export const seedCloud = async (connection) => {
|
|
108
|
+
await appendAdminConsoleRedirectUris(connection);
|
|
89
109
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AdminData } from '@logto/schemas';
|
|
1
|
+
import type { AdminData, UpdateAdminData, CreateScope } from '@logto/schemas';
|
|
2
2
|
import type { CommonQueryMethods } from 'slonik';
|
|
3
3
|
export declare const createTenant: (pool: CommonQueryMethods, tenantId: string) => Promise<void>;
|
|
4
|
-
export declare const seedAdminData: (pool: CommonQueryMethods, data: AdminData) => Promise<void>;
|
|
4
|
+
export declare const seedAdminData: (pool: CommonQueryMethods, data: AdminData | UpdateAdminData, ...additionalScopes: CreateScope[]) => Promise<void>;
|
|
5
|
+
export declare const assignScopesToRole: (pool: CommonQueryMethods, tenantId: string, roleId: string, ...scopeIds: string[]) => Promise<void>;
|
|
@@ -16,16 +16,38 @@ export const createTenant = async (pool, tenantId) => {
|
|
|
16
16
|
in role ${sql.identifier([parentRole])};
|
|
17
17
|
`);
|
|
18
18
|
};
|
|
19
|
-
export const seedAdminData = async (pool, data) => {
|
|
19
|
+
export const seedAdminData = async (pool, data, ...additionalScopes) => {
|
|
20
20
|
const { resource, scope, role } = data;
|
|
21
21
|
assert(resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId, new Error('All data should have the same tenant ID'));
|
|
22
|
+
const processRole = async () => {
|
|
23
|
+
if ('id' in role) {
|
|
24
|
+
await pool.query(insertInto(role, 'roles'));
|
|
25
|
+
return role.id;
|
|
26
|
+
}
|
|
27
|
+
// Query by role name for existing roles
|
|
28
|
+
const { id } = await pool.one(sql `
|
|
29
|
+
select id from roles
|
|
30
|
+
where name=${role.name}
|
|
31
|
+
and tenant_id=${String(role.tenantId)}
|
|
32
|
+
`);
|
|
33
|
+
return id;
|
|
34
|
+
};
|
|
22
35
|
await pool.query(insertInto(resource, 'resources'));
|
|
23
36
|
await pool.query(insertInto(scope, 'scopes'));
|
|
24
|
-
await pool.query(insertInto(
|
|
37
|
+
await Promise.all(additionalScopes.map(async (scope) => pool.query(insertInto(scope, 'scopes'))));
|
|
38
|
+
const roleId = await processRole();
|
|
25
39
|
await pool.query(insertInto({
|
|
26
40
|
id: generateStandardId(),
|
|
27
|
-
roleId
|
|
41
|
+
roleId,
|
|
28
42
|
scopeId: scope.id,
|
|
29
43
|
tenantId: resource.tenantId,
|
|
30
44
|
}, 'roles_scopes'));
|
|
31
45
|
};
|
|
46
|
+
export const assignScopesToRole = async (pool, tenantId, roleId, ...scopeIds) => {
|
|
47
|
+
await Promise.all(scopeIds.map(async (scopeId) => pool.query(insertInto({
|
|
48
|
+
id: generateStandardId(),
|
|
49
|
+
roleId,
|
|
50
|
+
scopeId,
|
|
51
|
+
tenantId,
|
|
52
|
+
}, 'roles_scopes'))));
|
|
53
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { systemGuards, systemKeys } from '@logto/schemas';
|
|
2
|
+
import { noop } from '@silverhand/essentials';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { createPoolFromConfig } from '../../database.js';
|
|
5
|
+
import { getRowByKey, updateValueByKey } from '../../queries/system.js';
|
|
6
|
+
import { log } from '../../utils.js';
|
|
7
|
+
const validKeysDisplay = chalk.green(systemKeys.join(', '));
|
|
8
|
+
const validateKey = (key) => {
|
|
9
|
+
// Using `.includes()` will result a type error
|
|
10
|
+
// eslint-disable-next-line unicorn/prefer-includes
|
|
11
|
+
if (!systemKeys.some((element) => element === key)) {
|
|
12
|
+
log.error(`Invalid config key ${chalk.red(key)} found, expected one of ${validKeysDisplay}`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const getConfig = {
|
|
16
|
+
command: 'get <key>',
|
|
17
|
+
describe: 'Get system value of the given key in Logto database',
|
|
18
|
+
builder: (yargs) => yargs.positional('key', {
|
|
19
|
+
describe: `The key to get from database system table, one of ${validKeysDisplay}`,
|
|
20
|
+
type: 'string',
|
|
21
|
+
demandOption: true,
|
|
22
|
+
}),
|
|
23
|
+
handler: async ({ key }) => {
|
|
24
|
+
validateKey(key);
|
|
25
|
+
const pool = await createPoolFromConfig();
|
|
26
|
+
const row = await getRowByKey(pool, key);
|
|
27
|
+
await pool.end();
|
|
28
|
+
const value = row?.value;
|
|
29
|
+
console.log(chalk.magenta(key) +
|
|
30
|
+
'=' +
|
|
31
|
+
(value === undefined ? chalk.gray(value) : chalk.green(JSON.stringify(value))));
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const setConfig = {
|
|
35
|
+
command: 'set <key> <value>',
|
|
36
|
+
describe: 'Set config value of the given key in Logto database',
|
|
37
|
+
builder: (yargs) => yargs
|
|
38
|
+
.positional('key', {
|
|
39
|
+
describe: `The key to get from database system table, one of ${validKeysDisplay}`,
|
|
40
|
+
type: 'string',
|
|
41
|
+
demandOption: true,
|
|
42
|
+
})
|
|
43
|
+
.positional('value', {
|
|
44
|
+
describe: 'The value to set, should be a valid JSON string',
|
|
45
|
+
type: 'string',
|
|
46
|
+
demandOption: true,
|
|
47
|
+
}),
|
|
48
|
+
handler: async ({ key, value }) => {
|
|
49
|
+
validateKey(key);
|
|
50
|
+
const guarded = systemGuards[key].parse(JSON.parse(value));
|
|
51
|
+
const pool = await createPoolFromConfig();
|
|
52
|
+
await updateValueByKey(pool, key, guarded);
|
|
53
|
+
await pool.end();
|
|
54
|
+
log.info(`Update ${chalk.green(key)} succeeded`);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const system = {
|
|
58
|
+
command: ['system'],
|
|
59
|
+
describe: 'Commands for Logto system config',
|
|
60
|
+
builder: (yargs) => yargs.command(getConfig).command(setConfig).demandCommand(1),
|
|
61
|
+
handler: noop,
|
|
62
|
+
};
|
|
63
|
+
export default system;
|
|
@@ -2,13 +2,13 @@ import type { CommandModule } from 'yargs';
|
|
|
2
2
|
export type InstallArgs = {
|
|
3
3
|
path?: string;
|
|
4
4
|
skipSeed: boolean;
|
|
5
|
-
|
|
5
|
+
cloud: boolean;
|
|
6
6
|
downloadUrl?: string;
|
|
7
7
|
};
|
|
8
8
|
declare const install: CommandModule<unknown, {
|
|
9
9
|
p?: string;
|
|
10
10
|
ss: boolean;
|
|
11
|
-
|
|
11
|
+
cloud: boolean;
|
|
12
12
|
du?: string;
|
|
13
13
|
}>;
|
|
14
14
|
export default install;
|
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { getDatabaseUrlFromConfig } from '../../database.js';
|
|
3
3
|
import { log } from '../../utils.js';
|
|
4
4
|
import { validateNodeVersion, inquireInstancePath, validateDatabase, downloadRelease, seedDatabase, createEnv, logFinale, decompress, isUrl, } from './utils.js';
|
|
5
|
-
const installLogto = async ({ path, skipSeed,
|
|
5
|
+
const installLogto = async ({ path, skipSeed, downloadUrl, cloud }) => {
|
|
6
6
|
validateNodeVersion();
|
|
7
7
|
// Get instance path
|
|
8
8
|
const instancePath = await inquireInstancePath(path);
|
|
@@ -16,7 +16,7 @@ const installLogto = async ({ path, skipSeed, officialConnectors, downloadUrl })
|
|
|
16
16
|
log.info(`Skipped database seeding.\n\n' + ' You can use the ${chalk.green('db seed')} command to seed database when ready.\n`);
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
await seedDatabase(instancePath);
|
|
19
|
+
await seedDatabase(instancePath, cloud);
|
|
20
20
|
}
|
|
21
21
|
// Save to dot env
|
|
22
22
|
await createEnv(instancePath, await getDatabaseUrlFromConfig());
|
|
@@ -38,10 +38,11 @@ const install = {
|
|
|
38
38
|
type: 'boolean',
|
|
39
39
|
default: false,
|
|
40
40
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
describe: 'Add official connectors after downloading Logto',
|
|
41
|
+
cloud: {
|
|
42
|
+
describe: 'Init Logto for cloud',
|
|
44
43
|
type: 'boolean',
|
|
44
|
+
hidden: true,
|
|
45
|
+
default: false,
|
|
45
46
|
},
|
|
46
47
|
du: {
|
|
47
48
|
alias: 'download-url',
|
|
@@ -50,8 +51,8 @@ const install = {
|
|
|
50
51
|
hidden: true,
|
|
51
52
|
},
|
|
52
53
|
}),
|
|
53
|
-
handler: async ({ p, ss,
|
|
54
|
-
await installLogto({ path: p, skipSeed: ss,
|
|
54
|
+
handler: async ({ p, ss, cloud, du }) => {
|
|
55
|
+
await installLogto({ path: p, skipSeed: ss, cloud, downloadUrl: du });
|
|
55
56
|
},
|
|
56
57
|
};
|
|
57
58
|
export default install;
|
|
@@ -4,7 +4,7 @@ export declare const inquireInstancePath: (initialPath?: string) => Promise<stri
|
|
|
4
4
|
export declare const validateDatabase: () => Promise<void>;
|
|
5
5
|
export declare const downloadRelease: (url?: string) => Promise<string>;
|
|
6
6
|
export declare const decompress: (toPath: string, tarPath: string) => Promise<void>;
|
|
7
|
-
export declare const seedDatabase: (instancePath: string) => Promise<void>;
|
|
7
|
+
export declare const seedDatabase: (instancePath: string, cloud: boolean) => Promise<void>;
|
|
8
8
|
export declare const createEnv: (instancePath: string, databaseUrl: string) => Promise<void>;
|
|
9
9
|
export declare const logFinale: (instancePath: string) => void;
|
|
10
10
|
export declare const isUrl: (string: string) => boolean;
|
|
@@ -92,10 +92,10 @@ export const decompress = async (toPath, tarPath) => {
|
|
|
92
92
|
prefixText: chalk.blue('[info]'),
|
|
93
93
|
}, true);
|
|
94
94
|
};
|
|
95
|
-
export const seedDatabase = async (instancePath) => {
|
|
95
|
+
export const seedDatabase = async (instancePath, cloud) => {
|
|
96
96
|
try {
|
|
97
97
|
const pool = await createPoolAndDatabaseIfNeeded();
|
|
98
|
-
await seedByPool(pool
|
|
98
|
+
await seedByPool(pool);
|
|
99
99
|
await pool.end();
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
|
2
|
+
export const notImplemented = () => {
|
|
3
|
+
throw new ConnectorError(ConnectorErrorCodes.NotImplemented);
|
|
4
|
+
};
|
|
5
|
+
export const defaultConnectorMethods = {
|
|
6
|
+
getAuthorizationUri: notImplemented,
|
|
7
|
+
getUserInfo: notImplemented,
|
|
8
|
+
sendMessage: notImplemented,
|
|
9
|
+
validateConfig: notImplemented,
|
|
10
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { notImplemented } from './consts.js';
|
|
3
|
+
import { loadConnector } from './loader.js';
|
|
4
|
+
import { parseMetadata, validateConnectorModule } from './utils.js';
|
|
5
|
+
// eslint-disable-next-line @silverhand/fp/no-let
|
|
6
|
+
let cachedConnectorFactories;
|
|
7
|
+
export const loadConnectorFactories = async (connectorPackages, ignoreVersionMismatch) => {
|
|
8
|
+
if (cachedConnectorFactories) {
|
|
9
|
+
return cachedConnectorFactories;
|
|
10
|
+
}
|
|
11
|
+
const connectorFactories = await Promise.all(connectorPackages.map(async ({ path: packagePath, name }) => {
|
|
12
|
+
try {
|
|
13
|
+
const createConnector = await loadConnector(packagePath, ignoreVersionMismatch);
|
|
14
|
+
const rawConnector = await createConnector({ getConfig: notImplemented });
|
|
15
|
+
validateConnectorModule(rawConnector);
|
|
16
|
+
return {
|
|
17
|
+
metadata: await parseMetadata(rawConnector.metadata, packagePath),
|
|
18
|
+
type: rawConnector.type,
|
|
19
|
+
createConnector,
|
|
20
|
+
path: packagePath,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error) {
|
|
25
|
+
console.log(`${chalk.red(`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`)}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
32
|
+
cachedConnectorFactories = connectorFactories.filter((connectorFactory) => connectorFactory !== undefined);
|
|
33
|
+
return cachedConnectorFactories;
|
|
34
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import connectorKitMeta from '@logto/connector-kit/package.json' assert { type: 'json' };
|
|
3
|
+
import { satisfies } from 'semver';
|
|
4
|
+
import { isKeyInObject } from './utils.js';
|
|
5
|
+
const connectorKit = '@logto/connector-kit';
|
|
6
|
+
const { version: currentVersion } = connectorKitMeta;
|
|
7
|
+
const checkConnectorKitVersion = (dependencies, ignoreVersionMismatch) => {
|
|
8
|
+
if (isKeyInObject(dependencies, connectorKit)) {
|
|
9
|
+
const value = dependencies[connectorKit];
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
if (satisfies(currentVersion, value)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const message = `Connector requires ${connectorKit} to be ${value}, but the version here is ${currentVersion}.`;
|
|
15
|
+
if (ignoreVersionMismatch) {
|
|
16
|
+
console.warn(`[warn] ${message}\n\nThis is highly discouraged in production.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Cannot find ${connectorKit} in connector's dependency`);
|
|
23
|
+
};
|
|
24
|
+
export const loadConnector = async (connectorPath, ignoreVersionMismatch) => {
|
|
25
|
+
const { default: { dependencies },
|
|
26
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
27
|
+
} = (await import(path.join(connectorPath, 'package.json'), {
|
|
28
|
+
assert: { type: 'json' },
|
|
29
|
+
}));
|
|
30
|
+
checkConnectorKitVersion(dependencies, ignoreVersionMismatch);
|
|
31
|
+
const loaded = await import(path.join(connectorPath, 'lib/index.js'));
|
|
32
|
+
if (isKeyInObject(loaded, 'default')) {
|
|
33
|
+
// CJS pattern
|
|
34
|
+
if (isKeyInObject(loaded.default, 'default')) {
|
|
35
|
+
if (typeof loaded.default.default === 'function') {
|
|
36
|
+
console.log(`[warn] Load connector ${connectorPath} in CJS mode`);
|
|
37
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
38
|
+
return loaded.default.default;
|
|
39
|
+
}
|
|
40
|
+
// ESM pattern
|
|
41
|
+
}
|
|
42
|
+
else if (typeof loaded.default === 'function') {
|
|
43
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
44
|
+
return loaded.default;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Cannot load connector from ${connectorPath}, the connector package may be broken.`);
|
|
48
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AllConnector, CreateConnector } from '@logto/connector-kit';
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic loaded connector type.
|
|
4
|
+
*/
|
|
5
|
+
export type ConnectorFactory<T extends AllConnector = AllConnector> = Pick<T, 'type' | 'metadata'> & {
|
|
6
|
+
createConnector: CreateConnector<T>;
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConnectorPackage = {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AllConnector, BaseConnector, ConnectorMetadata, GetConnectorConfig } from '@logto/connector-kit';
|
|
2
|
+
import { ConnectorType } from '@logto/connector-kit';
|
|
3
|
+
import type { ConnectorFactory } from './types.js';
|
|
4
|
+
export declare function validateConnectorModule(connector: Partial<BaseConnector<ConnectorType>>): asserts connector is BaseConnector<ConnectorType>;
|
|
5
|
+
export declare const readUrl: (url: string, baseUrl: string, type: 'text' | 'svg') => Promise<string>;
|
|
6
|
+
export declare const parseMetadata: (metadata: AllConnector['metadata'], packagePath: string) => Promise<AllConnector['metadata']>;
|
|
7
|
+
export declare const buildRawConnector: <T extends AllConnector = AllConnector>(connectorFactory: ConnectorFactory<T>, getConnectorConfig?: GetConnectorConfig) => Promise<{
|
|
8
|
+
rawConnector: T;
|
|
9
|
+
rawMetadata: ConnectorMetadata;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const isKeyInObject: <Key extends string>(object: unknown, key: Key) => object is Record<string, unknown> & Record<Key, unknown>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
|
|
5
|
+
import { notImplemented } from './consts.js';
|
|
6
|
+
export function validateConnectorModule(connector) {
|
|
7
|
+
if (!connector.metadata) {
|
|
8
|
+
throw new ConnectorError(ConnectorErrorCodes.InvalidMetadata);
|
|
9
|
+
}
|
|
10
|
+
if (!connector.configGuard) {
|
|
11
|
+
throw new ConnectorError(ConnectorErrorCodes.InvalidConfigGuard);
|
|
12
|
+
}
|
|
13
|
+
if (!connector.type || !Object.values(ConnectorType).includes(connector.type)) {
|
|
14
|
+
throw new ConnectorError(ConnectorErrorCodes.UnexpectedType);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const readUrl = async (url, baseUrl, type) => {
|
|
18
|
+
if (!url) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
if (type !== 'text' && url.startsWith('http')) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(path.join(baseUrl, url))) {
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
if (type === 'svg') {
|
|
28
|
+
const data = await readFile(path.join(baseUrl, url));
|
|
29
|
+
return `data:image/svg+xml;base64,${data.toString('base64')}`;
|
|
30
|
+
}
|
|
31
|
+
return readFile(path.join(baseUrl, url), 'utf8');
|
|
32
|
+
};
|
|
33
|
+
export const parseMetadata = async (metadata, packagePath) => {
|
|
34
|
+
return {
|
|
35
|
+
...metadata,
|
|
36
|
+
logo: await readUrl(metadata.logo, packagePath, 'svg'),
|
|
37
|
+
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
|
|
38
|
+
readme: await readUrl(metadata.readme, packagePath, 'text'),
|
|
39
|
+
configTemplate: metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const buildRawConnector = async (connectorFactory, getConnectorConfig) => {
|
|
43
|
+
const { createConnector, path: packagePath } = connectorFactory;
|
|
44
|
+
const rawConnector = await createConnector({
|
|
45
|
+
getConfig: getConnectorConfig ?? notImplemented,
|
|
46
|
+
});
|
|
47
|
+
validateConnectorModule(rawConnector);
|
|
48
|
+
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
|
|
49
|
+
return { rawConnector, rawMetadata };
|
|
50
|
+
};
|
|
51
|
+
export const isKeyInObject = (object, key) => object !== null && typeof object === 'object' && key in object;
|
package/lib/database.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { convertToPrimitiveOrSql } from '@logto/shared';
|
|
2
2
|
import { assert } from '@silverhand/essentials';
|
|
3
3
|
import decamelize from 'decamelize';
|
|
4
|
+
import { DatabaseError } from 'pg-protocol';
|
|
4
5
|
import { createPool, parseDsn, sql, stringifyDsn } from 'slonik';
|
|
5
6
|
import { createInterceptors } from 'slonik-interceptor-preset';
|
|
6
|
-
import { z } from 'zod';
|
|
7
7
|
import { ConfigKey, getCliConfigWithPrompt, log } from './utils.js';
|
|
8
8
|
export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto';
|
|
9
9
|
export const getDatabaseUrlFromConfig = async () => (await getCliConfigWithPrompt({
|
|
@@ -29,10 +29,9 @@ export const createPoolAndDatabaseIfNeeded = async () => {
|
|
|
29
29
|
return await createPoolFromConfig();
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
|
-
const result = z.object({ code: z.string() }).safeParse(error);
|
|
33
32
|
// Database does not exist, try to create one
|
|
34
33
|
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
35
|
-
if (!(
|
|
34
|
+
if (!(error instanceof DatabaseError && error.code === '3D000')) {
|
|
36
35
|
log.error(error);
|
|
37
36
|
}
|
|
38
37
|
const databaseUrl = await getDatabaseUrlFromConfig();
|
package/lib/package-json.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare const packageJson: {
|
|
|
40
40
|
url: string;
|
|
41
41
|
};
|
|
42
42
|
dependencies: {
|
|
43
|
+
"@logto/connector-kit": string;
|
|
43
44
|
"@logto/core-kit": string;
|
|
44
45
|
"@logto/schemas": string;
|
|
45
46
|
"@logto/shared": string;
|
|
@@ -54,6 +55,7 @@ export declare const packageJson: {
|
|
|
54
55
|
ora: string;
|
|
55
56
|
"p-limit": string;
|
|
56
57
|
"p-retry": string;
|
|
58
|
+
"pg-protocol": string;
|
|
57
59
|
roarr: string;
|
|
58
60
|
semver: string;
|
|
59
61
|
slonik: 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.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -43,10 +43,11 @@ export const packageJson = {
|
|
|
43
43
|
"url": "https://github.com/logto-io/logto/issues"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@logto/connector-kit": "workspace:*",
|
|
46
47
|
"@logto/core-kit": "workspace:*",
|
|
47
48
|
"@logto/schemas": "workspace:*",
|
|
48
49
|
"@logto/shared": "workspace:*",
|
|
49
|
-
"@silverhand/essentials": "2.
|
|
50
|
+
"@silverhand/essentials": "^2.5.0",
|
|
50
51
|
"chalk": "^5.0.0",
|
|
51
52
|
"decamelize": "^6.0.0",
|
|
52
53
|
"dotenv": "^16.0.0",
|
|
@@ -57,6 +58,7 @@ export const packageJson = {
|
|
|
57
58
|
"ora": "^6.1.2",
|
|
58
59
|
"p-limit": "^4.0.0",
|
|
59
60
|
"p-retry": "^5.1.2",
|
|
61
|
+
"pg-protocol": "^1.6.0",
|
|
60
62
|
"roarr": "^7.11.0",
|
|
61
63
|
"semver": "^7.3.8",
|
|
62
64
|
"slonik": "^30.0.0",
|
|
@@ -70,14 +72,14 @@ export const packageJson = {
|
|
|
70
72
|
"@silverhand/eslint-config": "2.0.1",
|
|
71
73
|
"@silverhand/ts-config": "2.0.3",
|
|
72
74
|
"@types/inquirer": "^9.0.0",
|
|
73
|
-
"@types/jest": "^29.
|
|
75
|
+
"@types/jest": "^29.4.0",
|
|
74
76
|
"@types/node": "^18.11.18",
|
|
75
77
|
"@types/semver": "^7.3.12",
|
|
76
78
|
"@types/sinon": "^10.0.13",
|
|
77
79
|
"@types/tar": "^6.1.2",
|
|
78
80
|
"@types/yargs": "^17.0.13",
|
|
79
81
|
"eslint": "^8.34.0",
|
|
80
|
-
"jest": "^29.
|
|
82
|
+
"jest": "^29.5.0",
|
|
81
83
|
"lint-staged": "^13.0.0",
|
|
82
84
|
"prettier": "^2.8.2",
|
|
83
85
|
"sinon": "^15.0.0",
|
package/lib/queries/system.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import type { System, SystemKey } from '@logto/schemas';
|
|
2
|
+
import { systemGuards } from '@logto/schemas';
|
|
1
3
|
import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik';
|
|
4
|
+
import type { z } from 'zod';
|
|
2
5
|
export declare const doesSystemsTableExist: (pool: CommonQueryMethods) => Promise<boolean>;
|
|
3
6
|
export declare const getCurrentDatabaseAlterationTimestamp: (pool: CommonQueryMethods) => Promise<number>;
|
|
4
7
|
export declare const updateDatabaseTimestamp: (connection: DatabaseTransactionConnection, timestamp: number) => Promise<void>;
|
|
8
|
+
export declare const getRowByKey: (pool: CommonQueryMethods, key: SystemKey) => Promise<System | null>;
|
|
9
|
+
export declare const updateValueByKey: <T extends SystemKey>(pool: CommonQueryMethods, key: T, value: z.TypeOf<import("@logto/schemas").SystemGuard[T]>) => Promise<import("slonik").QueryResult<import("slonik").QueryResultRow>>;
|
package/lib/queries/system.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { systemGuards, Systems, AlterationStateKey } from '@logto/schemas';
|
|
2
2
|
import { convertToIdentifiers } from '@logto/shared';
|
|
3
|
+
import { DatabaseError } from 'pg-protocol';
|
|
3
4
|
import { sql } from 'slonik';
|
|
4
|
-
|
|
5
|
-
const { fields } = convertToIdentifiers(Systems);
|
|
5
|
+
const { fields, table } = convertToIdentifiers(Systems);
|
|
6
6
|
const doesTableExist = async (pool, table) => {
|
|
7
7
|
const { rows } = await pool.query(sql `select to_regclass(${table}) as regclass`);
|
|
8
8
|
return Boolean(rows[0]?.regclass);
|
|
@@ -20,10 +20,9 @@ export const getCurrentDatabaseAlterationTimestamp = async (pool) => {
|
|
|
20
20
|
return (parsed.success && parsed.data.timestamp) || 0;
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
|
-
const result = z.object({ code: z.string() }).safeParse(error);
|
|
24
23
|
// Relation does not exist, treat as 0
|
|
25
24
|
// https://www.postgresql.org/docs/14/errcodes-appendix.html
|
|
26
|
-
if (
|
|
25
|
+
if (error instanceof DatabaseError && error.code === '42P01') {
|
|
27
26
|
return 0;
|
|
28
27
|
}
|
|
29
28
|
throw error;
|
|
@@ -41,3 +40,13 @@ export const updateDatabaseTimestamp = async (connection, timestamp) => {
|
|
|
41
40
|
on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value}
|
|
42
41
|
`);
|
|
43
42
|
};
|
|
43
|
+
export const getRowByKey = async (pool, key) => pool.maybeOne(sql `
|
|
44
|
+
select ${sql.join([fields.key, fields.value], sql `,`)} from ${table}
|
|
45
|
+
where ${fields.key} = ${key}
|
|
46
|
+
`);
|
|
47
|
+
export const updateValueByKey = async (pool, key, value) => pool.query(sql `
|
|
48
|
+
insert into ${table} (${fields.key}, ${fields.value})
|
|
49
|
+
values (${key}, ${sql.jsonb(value)})
|
|
50
|
+
on conflict (${fields.key})
|
|
51
|
+
do update set ${fields.value}=excluded.${fields.value}
|
|
52
|
+
`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AlterationStateKey, Systems } from '@logto/schemas';
|
|
2
2
|
import { convertToIdentifiers } from '@logto/shared';
|
|
3
|
+
import { DatabaseError } from 'pg-protocol';
|
|
3
4
|
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
|
4
5
|
import { expectSqlAssert } from '../test-utils.js';
|
|
5
6
|
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './system.js';
|
|
@@ -15,7 +16,10 @@ const timestamp = 1_663_923_776;
|
|
|
15
16
|
const systemsTableExists = async () => createMockQueryResult([{ regclass: true }]);
|
|
16
17
|
describe('getCurrentDatabaseAlterationTimestamp()', () => {
|
|
17
18
|
it('returns 0 if query failed (table not found)', async () => {
|
|
18
|
-
|
|
19
|
+
const error = new DatabaseError('test', 0, 'noData');
|
|
20
|
+
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
21
|
+
error.code = '42P01';
|
|
22
|
+
mockQuery.mockImplementationOnce(systemsTableExists).mockRejectedValueOnce(error);
|
|
19
23
|
await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0);
|
|
20
24
|
});
|
|
21
25
|
it('returns 0 if the row is not found', async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Logto CLI.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"url": "https://github.com/logto-io/logto/issues"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@silverhand/essentials": "2.
|
|
31
|
+
"@silverhand/essentials": "^2.5.0",
|
|
32
32
|
"chalk": "^5.0.0",
|
|
33
33
|
"decamelize": "^6.0.0",
|
|
34
34
|
"dotenv": "^16.0.0",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"ora": "^6.1.2",
|
|
40
40
|
"p-limit": "^4.0.0",
|
|
41
41
|
"p-retry": "^5.1.2",
|
|
42
|
+
"pg-protocol": "^1.6.0",
|
|
42
43
|
"roarr": "^7.11.0",
|
|
43
44
|
"semver": "^7.3.8",
|
|
44
45
|
"slonik": "^30.0.0",
|
|
@@ -47,22 +48,23 @@
|
|
|
47
48
|
"tar": "^6.1.11",
|
|
48
49
|
"yargs": "^17.6.0",
|
|
49
50
|
"zod": "^3.20.2",
|
|
50
|
-
"@logto/
|
|
51
|
-
"@logto/
|
|
52
|
-
"@logto/
|
|
51
|
+
"@logto/connector-kit": "1.1.0",
|
|
52
|
+
"@logto/core-kit": "1.1.0",
|
|
53
|
+
"@logto/schemas": "1.0.0",
|
|
54
|
+
"@logto/shared": "1.0.0"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@silverhand/eslint-config": "2.0.1",
|
|
56
58
|
"@silverhand/ts-config": "2.0.3",
|
|
57
59
|
"@types/inquirer": "^9.0.0",
|
|
58
|
-
"@types/jest": "^29.
|
|
60
|
+
"@types/jest": "^29.4.0",
|
|
59
61
|
"@types/node": "^18.11.18",
|
|
60
62
|
"@types/semver": "^7.3.12",
|
|
61
63
|
"@types/sinon": "^10.0.13",
|
|
62
64
|
"@types/tar": "^6.1.2",
|
|
63
65
|
"@types/yargs": "^17.0.13",
|
|
64
66
|
"eslint": "^8.34.0",
|
|
65
|
-
"jest": "^29.
|
|
67
|
+
"jest": "^29.5.0",
|
|
66
68
|
"lint-staged": "^13.0.0",
|
|
67
69
|
"prettier": "^2.8.2",
|
|
68
70
|
"sinon": "^15.0.0",
|
package/LICENSE.cloud
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
Elastic License 2.0
|
|
2
|
-
|
|
3
|
-
URL: https://www.elastic.co/licensing/elastic-license
|
|
4
|
-
|
|
5
|
-
## Acceptance
|
|
6
|
-
|
|
7
|
-
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
-
|
|
9
|
-
## Copyright License
|
|
10
|
-
|
|
11
|
-
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
12
|
-
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
13
|
-
available, and prepare derivative works of the software, in each case subject to
|
|
14
|
-
the limitations and conditions below.
|
|
15
|
-
|
|
16
|
-
## Limitations
|
|
17
|
-
|
|
18
|
-
You may not provide the software to third parties as a hosted or managed
|
|
19
|
-
service, where the service provides users with access to any substantial set of
|
|
20
|
-
the features or functionality of the software.
|
|
21
|
-
|
|
22
|
-
You may not move, change, disable, or circumvent the license key functionality
|
|
23
|
-
in the software, and you may not remove or obscure any functionality in the
|
|
24
|
-
software that is protected by the license key.
|
|
25
|
-
|
|
26
|
-
You may not alter, remove, or obscure any licensing, copyright, or other notices
|
|
27
|
-
of the licensor in the software. Any use of the licensor’s trademarks is subject
|
|
28
|
-
to applicable law.
|
|
29
|
-
|
|
30
|
-
## Patents
|
|
31
|
-
|
|
32
|
-
The licensor grants you a license, under any patent claims the licensor can
|
|
33
|
-
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
34
|
-
sale, import and have imported the software, in each case subject to the
|
|
35
|
-
limitations and conditions in this license. This license does not cover any
|
|
36
|
-
patent claims that you cause to be infringed by modifications or additions to
|
|
37
|
-
the software. If you or your company make any written claim that the software
|
|
38
|
-
infringes or contributes to infringement of any patent, your patent license for
|
|
39
|
-
the software granted under these terms ends immediately. If your company makes
|
|
40
|
-
such a claim, your patent license ends immediately for work on behalf of your
|
|
41
|
-
company.
|
|
42
|
-
|
|
43
|
-
## Notices
|
|
44
|
-
|
|
45
|
-
You must ensure that anyone who gets a copy of any part of the software from you
|
|
46
|
-
also gets a copy of these terms.
|
|
47
|
-
|
|
48
|
-
If you modify the software, you must include in any modified copies of the
|
|
49
|
-
software prominent notices stating that you have modified the software.
|
|
50
|
-
|
|
51
|
-
## No Other Rights
|
|
52
|
-
|
|
53
|
-
These terms do not imply any licenses other than those expressly granted in
|
|
54
|
-
these terms.
|
|
55
|
-
|
|
56
|
-
## Termination
|
|
57
|
-
|
|
58
|
-
If you use the software in violation of these terms, such use is not licensed,
|
|
59
|
-
and your licenses will automatically terminate. If the licensor provides you
|
|
60
|
-
with a notice of your violation, and you cease all violation of this license no
|
|
61
|
-
later than 30 days after you receive that notice, your licenses will be
|
|
62
|
-
reinstated retroactively. However, if you violate these terms after such
|
|
63
|
-
reinstatement, any additional violation of these terms will cause your licenses
|
|
64
|
-
to terminate automatically and permanently.
|
|
65
|
-
|
|
66
|
-
## No Liability
|
|
67
|
-
|
|
68
|
-
*As far as the law allows, the software comes as is, without any warranty or
|
|
69
|
-
condition, and the licensor will not be liable to you for any damages arising
|
|
70
|
-
out of these terms or the use or nature of the software, under any kind of
|
|
71
|
-
legal claim.*
|
|
72
|
-
|
|
73
|
-
## Definitions
|
|
74
|
-
|
|
75
|
-
The **licensor** is the entity offering these terms, and the **software** is the
|
|
76
|
-
software the licensor makes available under these terms, including any portion
|
|
77
|
-
of it.
|
|
78
|
-
|
|
79
|
-
**you** refers to the individual or entity agreeing to these terms.
|
|
80
|
-
|
|
81
|
-
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
82
|
-
organization that you work for, plus all organizations that have control over,
|
|
83
|
-
are under the control of, or are under common control with that
|
|
84
|
-
organization. **control** means ownership of substantially all the assets of an
|
|
85
|
-
entity, or the power to direct its management and policies by vote, contract, or
|
|
86
|
-
otherwise. Control can be direct or indirect.
|
|
87
|
-
|
|
88
|
-
**your licenses** are all the licenses granted to you for the software under
|
|
89
|
-
these terms.
|
|
90
|
-
|
|
91
|
-
**use** means anything you do with the software requiring one of your licenses.
|
|
92
|
-
|
|
93
|
-
**trademark** means trademarks, service marks, and similar rights.
|