0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(schemas,cloud): align tenant types (#4004)

This commit is contained in:
Darcy Ye 2023-06-12 18:36:15 +08:00 committed by GitHub
parent d2f7c94167
commit e96e92b2df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 79 additions and 109 deletions

View file

@ -1,11 +1,5 @@
import { createTenantMetadata } from '@logto/core-kit'; import { createTenantMetadata } from '@logto/core-kit';
import type { import type { AdminData, UpdateAdminData, CreateScope, CreateRolesScope } from '@logto/schemas';
CreateTenant,
AdminData,
UpdateAdminData,
CreateScope,
CreateRolesScope,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared'; import { generateStandardId } from '@logto/shared';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import type { CommonQueryMethods } from 'slonik'; import type { CommonQueryMethods } from 'slonik';
@ -18,7 +12,11 @@ import { getDatabaseName } from '../../../queries/database.js';
export const createTenant = async (pool: CommonQueryMethods, tenantId: string) => { export const createTenant = async (pool: CommonQueryMethods, tenantId: string) => {
const database = await getDatabaseName(pool, true); const database = await getDatabaseName(pool, true);
const { parentRole, role, password } = createTenantMetadata(database, tenantId); const { parentRole, role, password } = createTenantMetadata(database, tenantId);
const createTenant: CreateTenant = { id: tenantId, dbUser: role, dbUserPassword: password }; const createTenant = {
id: tenantId,
dbUser: role,
dbUserPassword: password,
};
await pool.query(insertInto(createTenant, 'tenants')); await pool.query(insertInto(createTenant, 'tenants'));
await pool.query(sql` await pool.query(sql`

View file

@ -31,7 +31,7 @@
"@logto/shared": "workspace:^2.0.0", "@logto/shared": "workspace:^2.0.0",
"@silverhand/essentials": "^2.5.0", "@silverhand/essentials": "^2.5.0",
"@withtyped/postgres": "^0.11.0", "@withtyped/postgres": "^0.11.0",
"@withtyped/server": "^0.11.0", "@withtyped/server": "^0.11.1",
"accepts": "^1.3.8", "accepts": "^1.3.8",
"chalk": "^5.0.0", "chalk": "^5.0.0",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",

View file

@ -6,8 +6,6 @@ import { DemoConnector } from '@logto/connector-kit';
import { createTenantMetadata } from '@logto/core-kit'; import { createTenantMetadata } from '@logto/core-kit';
import { import {
type LogtoOidcConfigType, type LogtoOidcConfigType,
type TenantInfo,
type CreateTenant,
createAdminTenantApplicationRole, createAdminTenantApplicationRole,
AdminTenantRole, AdminTenantRole,
createTenantMachineToMachineApplication, createTenantMachineToMachineApplication,
@ -20,8 +18,9 @@ import {
createAdminData, createAdminData,
createAdminDataInAdminTenant, createAdminDataInAdminTenant,
getManagementApiResourceIndicator, getManagementApiResourceIndicator,
type PatchTenant, type TenantModel,
} from '@logto/schemas'; } from '@logto/schemas';
import type { TenantInfo } from '@logto/schemas/models';
import { generateStandardId } from '@logto/shared'; import { generateStandardId } from '@logto/shared';
import { appendPath } from '@silverhand/essentials'; import { appendPath } from '@silverhand/essentials';
@ -71,7 +70,10 @@ export class TenantsLibrary {
})); }));
} }
async updateTenantById(tenantId: string, payload: PatchTenant): Promise<TenantInfo> { async updateTenantById(
tenantId: string,
payload: Partial<Pick<TenantModel, 'name' | 'tag'>>
): Promise<TenantInfo> {
const { id, name, tag } = await this.queries.tenants.updateTenantById(tenantId, payload); const { id, name, tag } = await this.queries.tenants.updateTenantById(tenantId, payload);
return { id, name, tag, indicator: getManagementApiResourceIndicator(id) }; return { id, name, tag, indicator: getManagementApiResourceIndicator(id) };
@ -119,13 +121,13 @@ export class TenantsLibrary {
async createNewTenant( async createNewTenant(
forUserId: string, forUserId: string,
payload: Pick<CreateTenant, 'name' | 'tag'> payload: Partial<Pick<TenantModel, 'name' | 'tag'>>
): Promise<TenantInfo> { ): Promise<TenantInfo> {
const databaseName = await getDatabaseName(this.queries.client); const databaseName = await getDatabaseName(this.queries.client);
const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName); const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName);
// Init tenant // Init tenant
const createTenant: CreateTenant = { const createTenant = {
id: tenantId, id: tenantId,
dbUser: role, dbUser: role,
dbUserPassword: password, dbUserPassword: password,

View file

@ -8,11 +8,9 @@ import {
PredefinedScope, PredefinedScope,
ApplicationType, ApplicationType,
type AdminData, type AdminData,
type CreateTenant,
type PatchTenant,
type CreateRolesScope, type CreateRolesScope,
type TenantModel,
} from '@logto/schemas'; } from '@logto/schemas';
import type { TenantModel } from '@logto/schemas/models';
import { generateStandardId } from '@logto/shared'; import { generateStandardId } from '@logto/shared';
import { import {
type PostgreSql, type PostgreSql,
@ -47,20 +45,15 @@ export const createTenantsQueries = (client: Queryable<PostgreSql>) => {
where roles.tenant_id = ${adminTenantId}; where roles.tenant_id = ${adminTenantId};
`); `);
const insertTenant = async (tenant: CreateTenant) => const insertTenant = async (
client.query( tenant: Pick<TenantModel, 'id' | 'dbUser' | 'dbUserPassword'> &
insertInto( Partial<Pick<TenantModel, 'name' | 'tag'>>
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ) => client.query(insertInto(tenant, 'tenants'));
Object.fromEntries(Object.entries(tenant).filter(([_, value]) => value !== undefined)),
'tenants'
)
);
const updateTenantById = async (tenantId: string, rawPayload: PatchTenant) => { const updateTenantById = async (
const payload: Record<string, string> = Object.fromEntries( tenantId: string,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition payload: Partial<Pick<TenantModel, 'name' | 'tag'>>
Object.entries(rawPayload).filter(([_, value]) => value !== undefined) ) => {
);
const tenant = await client.maybeOne<TenantModel>(sql` const tenant = await client.maybeOne<TenantModel>(sql`
update tenants update tenants
set ${Object.entries(payload).map(([key, value]) => sql`${id(key)}=${jsonIfNeeded(value)}`)} set ${Object.entries(payload).map(([key, value]) => sql`${id(key)}=${jsonIfNeeded(value)}`)}

View file

@ -1,5 +1,5 @@
import type { TenantInfo } from '@logto/schemas'; import { CloudScope } from '@logto/schemas';
import { CloudScope, TenantTag } from '@logto/schemas'; import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/context.js'; import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/context.js';
import { noop } from '#src/test-utils/function.js'; import { noop } from '#src/test-utils/function.js';

View file

@ -1,10 +1,5 @@
import { import { CloudScope, adminTenantId, defaultTenantId } from '@logto/schemas';
CloudScope, import { Tenants, tenantInfoGuard } from '@logto/schemas/models';
tenantInfoGuard,
createTenantGuard,
adminTenantId,
defaultTenantId,
} from '@logto/schemas';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import { createRouter, RequestError } from '@withtyped/server'; import { createRouter, RequestError } from '@withtyped/server';
@ -23,7 +18,7 @@ export const tenantsRoutes = (library: TenantsLibrary) =>
.patch( .patch(
'/:tenantId', '/:tenantId',
{ {
body: createTenantGuard.pick({ name: true, tag: true }).partial(), body: Tenants.guard('patch').pick({ name: true, tag: true }),
response: tenantInfoGuard, response: tenantInfoGuard,
}, },
async (context, next) => { async (context, next) => {
@ -61,7 +56,7 @@ export const tenantsRoutes = (library: TenantsLibrary) =>
.post( .post(
'/', '/',
{ {
body: createTenantGuard.pick({ name: true, tag: true }).required(), body: Tenants.guard('create').pick({ name: true, tag: true }),
response: tenantInfoGuard, response: tenantInfoGuard,
}, },
async (context, next) => { async (context, next) => {

View file

@ -1,4 +1,5 @@
import type { ServiceLogType, TenantInfo, TenantTag } from '@logto/schemas'; import type { ServiceLogType } from '@logto/schemas';
import type { TenantInfo, TenantTag } from '@logto/schemas/models';
import type { ServicesLibrary } from '#src/libraries/services.js'; import type { ServicesLibrary } from '#src/libraries/services.js';
import type { TenantsLibrary } from '#src/libraries/tenants.js'; import type { TenantsLibrary } from '#src/libraries/tenants.js';

View file

@ -1,5 +1,5 @@
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import type { TenantInfo } from '@logto/schemas'; import type { TenantInfo } from '@logto/schemas/models';
import { trySafe } from '@silverhand/essentials'; import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { useHref } from 'react-router-dom'; import { useHref } from 'react-router-dom';

View file

@ -1,4 +1,4 @@
import { type TenantInfo, TenantTag } from '@logto/schemas'; import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import { useCallback, useContext, useEffect } from 'react'; import { useCallback, useContext, useEffect } from 'react';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';

View file

@ -1,5 +1,5 @@
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import type { TenantInfo } from '@logto/schemas'; import type { TenantInfo } from '@logto/schemas/models';
import { conditional, yes } from '@silverhand/essentials'; import { conditional, yes } from '@silverhand/essentials';
import { HTTPError } from 'ky'; import { HTTPError } from 'ky';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';

View file

@ -1,4 +1,5 @@
import { type TenantInfo, TenantTag, defaultManagementApi } from '@logto/schemas'; import { defaultManagementApi } from '@logto/schemas';
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import { conditional, noop } from '@silverhand/essentials'; import { conditional, noop } from '@silverhand/essentials';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useMemo, createContext, useState } from 'react'; import { useCallback, useMemo, createContext, useState } from 'react';
@ -24,7 +25,7 @@ type Tenants = {
const { tenantId, indicator } = defaultManagementApi.resource; const { tenantId, indicator } = defaultManagementApi.resource;
const initialTenants = conditional( const initialTenants = conditional(
!isCloud && [ !isCloud && [
{ id: tenantId, name: `tenant_${tenantId}`, tag: `${TenantTag.Development}`, indicator }, // Make `tag` value to be string type. { id: tenantId, name: `tenant_${tenantId}`, tag: TenantTag.Development, indicator }, // Make `tag` value to be string type.
] ]
); );

View file

@ -1,5 +1,5 @@
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import { type TenantInfo } from '@logto/schemas'; import { type TenantInfo } from '@logto/schemas/models';
import { type Optional, trySafe } from '@silverhand/essentials'; import { type Optional, trySafe } from '@silverhand/essentials';
import type ky from 'ky'; import type ky from 'ky';
import { useCallback, useContext, useEffect, useMemo } from 'react'; import { useCallback, useContext, useEffect, useMemo } from 'react';

View file

@ -1,4 +1,4 @@
import { type TenantInfo } from '@logto/schemas'; import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';

View file

@ -1,5 +1,5 @@
import type { AdminConsoleKey } from '@logto/phrases'; import type { AdminConsoleKey } from '@logto/phrases';
import { TenantTag } from '@logto/schemas'; import { TenantTag } from '@logto/schemas/models';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View file

@ -1,4 +1,4 @@
import { type PatchTenant, type TenantInfo, TenantTag } from '@logto/schemas'; import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
@ -49,7 +49,7 @@ function TenantBasicSettings() {
reset({ profile: { name, tag } }); reset({ profile: { name, tag } });
}, [currentTenant, reset]); }, [currentTenant, reset]);
const saveData = async (data: PatchTenant) => { const saveData = async (data: { name?: string; tag?: TenantTag }) => {
try { try {
const { name, tag } = await cloudApi const { name, tag } = await cloudApi
.patch(`/api/tenants/${currentTenantId}`, { .patch(`/api/tenants/${currentTenantId}`, {

View file

@ -1,5 +1,5 @@
import { type TenantInfo } from '@logto/schemas'; import { type TenantModel } from '@logto/schemas/models';
export type TenantSettingsForm = { export type TenantSettingsForm = {
profile: Pick<TenantInfo, 'name' | 'tag'>; profile: Pick<TenantModel, 'name' | 'tag'>;
}; };

View file

@ -1,10 +1,10 @@
import type { CreateTenant, TenantInfo, TenantTag } from '@logto/schemas'; import type { TenantInfo, TenantTag } from '@logto/schemas/models';
import { cloudApi } from './api.js'; import { cloudApi } from './api.js';
export const createTenant = async ( export const createTenant = async (
accessToken: string, accessToken: string,
payload: Required<Pick<CreateTenant, 'name' | 'tag'>> payload: { name: string; tag: TenantTag }
) => { ) => {
return cloudApi return cloudApi
.extend({ .extend({

View file

@ -11,11 +11,9 @@ import {
type Resource, type Resource,
type Scope, type Scope,
type Role, type Role,
TenantTag,
type TenantInfo,
type CreateTenant,
defaultTenantId, defaultTenantId,
} from '@logto/schemas'; } from '@logto/schemas';
import { TenantTag, type TenantInfo } from '@logto/schemas/models';
import { GlobalValues } from '@logto/shared'; import { GlobalValues } from '@logto/shared';
import { appendPath } from '@silverhand/essentials'; import { appendPath } from '@silverhand/essentials';
@ -40,7 +38,7 @@ describe('Tenant APIs', () => {
for (const [payload, tenant] of [ for (const [payload, tenant] of [
[payload1, tenant1], [payload1, tenant1],
[payload2, tenant2], [payload2, tenant2],
] as Array<[Required<Pick<CreateTenant, 'name' | 'tag'>>, TenantInfo]>) { ] as Array<[{ name: string; tag: TenantTag }, TenantInfo]>) {
expect(tenant).toHaveProperty('id'); expect(tenant).toHaveProperty('id');
expect(tenant).toHaveProperty('tag', payload.tag); expect(tenant).toHaveProperty('tag', payload.tag);
expect(tenant).toHaveProperty('name', payload.name); expect(tenant).toHaveProperty('name', payload.name);
@ -82,7 +80,7 @@ describe('Tenant APIs', () => {
[payload1, tenant1], [payload1, tenant1],
[payload2, tenant2], [payload2, tenant2],
[payload3, tenant3], [payload3, tenant3],
] as Array<[Required<Pick<CreateTenant, 'name' | 'tag'>>, TenantInfo]>) { ] as Array<[{ name: string; tag: TenantTag }, TenantInfo]>) {
expect(tenant).toHaveProperty('id'); expect(tenant).toHaveProperty('id');
expect(tenant).toHaveProperty('tag', payload.tag); expect(tenant).toHaveProperty('tag', payload.tag);
expect(tenant).toHaveProperty('name', payload.name); expect(tenant).toHaveProperty('name', payload.name);

View file

@ -86,7 +86,7 @@
"@logto/phrases": "workspace:^1.4.0", "@logto/phrases": "workspace:^1.4.0",
"@logto/phrases-ui": "workspace:^1.2.0", "@logto/phrases-ui": "workspace:^1.2.0",
"@logto/shared": "workspace:^2.0.0", "@logto/shared": "workspace:^2.0.0",
"@withtyped/server": "^0.11.0", "@withtyped/server": "^0.11.1",
"zod": "^3.20.2" "zod": "^3.20.2"
} }
} }

View file

@ -1,6 +1,12 @@
import { createModel } from '@withtyped/server/model'; import { createModel } from '@withtyped/server/model';
import type { InferModelType } from '@withtyped/server/model';
import { z } from 'zod';
import { TenantTag } from '../index.js'; export enum TenantTag {
Development = 'development',
Staging = 'staging',
Production = 'production',
}
export const Tenants = createModel(/* sql */ ` export const Tenants = createModel(/* sql */ `
/* init_order = 0 */ /* init_order = 0 */
@ -16,4 +22,14 @@ export const Tenants = createModel(/* sql */ `
unique (db_user) unique (db_user)
); );
/* no_after_each */ /* no_after_each */
`); `)
.extend('tag', z.nativeEnum(TenantTag))
.extend('createdAt', { readonly: true });
export type TenantModel = InferModelType<typeof Tenants>;
export type TenantInfo = Pick<TenantModel, 'id' | 'name' | 'tag'> & { indicator: string };
export const tenantInfoGuard: z.ZodType<TenantInfo> = Tenants.guard('model')
.pick({ id: true, name: true, tag: true })
.extend({ indicator: z.string() });

View file

@ -1,8 +1,4 @@
import type { InferModelType } from '@withtyped/server/model'; export type { TenantModel } from '../models/tenants.js';
import type { Tenants } from '../models/tenants.js';
export const defaultTenantId = 'default'; export const defaultTenantId = 'default';
export const adminTenantId = 'admin'; export const adminTenantId = 'admin';
export type TenantModel = InferModelType<typeof Tenants>;

View file

@ -11,7 +11,6 @@ export * from './role.js';
export * from './verification-code.js'; export * from './verification-code.js';
export * from './application.js'; export * from './application.js';
export * from './system.js'; export * from './system.js';
export * from './tenant.js';
export * from './user-assets.js'; export * from './user-assets.js';
export * from './hook.js'; export * from './hook.js';
export * from './service-log.js'; export * from './service-log.js';

View file

@ -1,29 +0,0 @@
import { z } from 'zod';
import { type TenantModel } from '../seeds/tenant.js';
export enum TenantTag {
Development = 'development',
Staging = 'staging',
Production = 'production',
}
export type PatchTenant = Partial<Pick<TenantModel, 'name' | 'tag'>>;
export type CreateTenant = Pick<TenantModel, 'id' | 'dbUser' | 'dbUserPassword'> &
PatchTenant & { createdAt?: number };
export const createTenantGuard = z.object({
id: z.string(),
dbUser: z.string(),
dbUserPassword: z.string(),
name: z.string().optional(),
tag: z.nativeEnum(TenantTag).optional(),
createdAt: z.number().optional(),
});
export type TenantInfo = Pick<TenantModel, 'id' | 'name' | 'tag'> & { indicator: string };
export const tenantInfoGuard: z.ZodType<TenantInfo> = createTenantGuard
.pick({ id: true, name: true, tag: true })
.extend({ indicator: z.string() })
.required();

View file

@ -253,10 +253,10 @@ importers:
version: 2.5.0 version: 2.5.0
'@withtyped/postgres': '@withtyped/postgres':
specifier: ^0.11.0 specifier: ^0.11.0
version: 0.11.0(@withtyped/server@0.11.0) version: 0.11.0(@withtyped/server@0.11.1)
'@withtyped/server': '@withtyped/server':
specifier: ^0.11.0 specifier: ^0.11.1
version: 0.11.0(zod@3.20.2) version: 0.11.1(zod@3.20.2)
accepts: accepts:
specifier: ^1.3.8 specifier: ^1.3.8
version: 1.3.8 version: 1.3.8
@ -3556,8 +3556,8 @@ importers:
specifier: workspace:^2.0.0 specifier: workspace:^2.0.0
version: link:../shared version: link:../shared
'@withtyped/server': '@withtyped/server':
specifier: ^0.11.0 specifier: ^0.11.1
version: 0.11.0(zod@3.20.2) version: 0.11.1(zod@3.20.2)
zod: zod:
specifier: ^3.20.2 specifier: ^3.20.2
version: 3.20.2 version: 3.20.2
@ -9808,21 +9808,21 @@ packages:
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true dev: true
/@withtyped/postgres@0.11.0(@withtyped/server@0.11.0): /@withtyped/postgres@0.11.0(@withtyped/server@0.11.1):
resolution: {integrity: sha512-PHnx6ake/MDdyy4sZXS/7l5XNBtjqlPSqSHrlmCYUXYxUV0sHSrXECKxX7deAvWZtcHVh9VaWEpiQBhFS06Vig==} resolution: {integrity: sha512-PHnx6ake/MDdyy4sZXS/7l5XNBtjqlPSqSHrlmCYUXYxUV0sHSrXECKxX7deAvWZtcHVh9VaWEpiQBhFS06Vig==}
peerDependencies: peerDependencies:
'@withtyped/server': ^0.11.0 '@withtyped/server': ^0.11.0
dependencies: dependencies:
'@types/pg': 8.6.6 '@types/pg': 8.6.6
'@withtyped/server': 0.11.0(zod@3.20.2) '@withtyped/server': 0.11.1(zod@3.20.2)
'@withtyped/shared': 0.2.0 '@withtyped/shared': 0.2.0
pg: 8.8.0 pg: 8.8.0
transitivePeerDependencies: transitivePeerDependencies:
- pg-native - pg-native
dev: false dev: false
/@withtyped/server@0.11.0(zod@3.20.2): /@withtyped/server@0.11.1(zod@3.20.2):
resolution: {integrity: sha512-InF3g9uv63ImK/sTIywcwX9tg2KnyFOlYsqeVI1WDjV1eTdzVxRJInD8iA08+U10S/dhBimukSx2Bfjwt8MWiw==} resolution: {integrity: sha512-9b/rH+6S8rKlgVTuDfDOSXiVjOBz5yd4EIP6qswncIJvA+i9j1ykR0GIBGuJSQAr7SWrSNCILUVK+oDTiWzJ6g==}
peerDependencies: peerDependencies:
zod: ^3.19.1 zod: ^3.19.1
dependencies: dependencies: