diff --git a/packages/cli/src/commands/database/seed/tenant.ts b/packages/cli/src/commands/database/seed/tenant.ts index 0f7830ec7..396a8f95e 100644 --- a/packages/cli/src/commands/database/seed/tenant.ts +++ b/packages/cli/src/commands/database/seed/tenant.ts @@ -1,11 +1,5 @@ import { createTenantMetadata } from '@logto/core-kit'; -import type { - CreateTenant, - AdminData, - UpdateAdminData, - CreateScope, - CreateRolesScope, -} from '@logto/schemas'; +import type { AdminData, UpdateAdminData, CreateScope, CreateRolesScope } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { assert } from '@silverhand/essentials'; import type { CommonQueryMethods } from 'slonik'; @@ -18,7 +12,11 @@ import { getDatabaseName } from '../../../queries/database.js'; export const createTenant = async (pool: CommonQueryMethods, tenantId: string) => { const database = await getDatabaseName(pool, true); 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(sql` diff --git a/packages/cloud/package.json b/packages/cloud/package.json index f54f39107..ba8a7f739 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -31,7 +31,7 @@ "@logto/shared": "workspace:^2.0.0", "@silverhand/essentials": "^2.5.0", "@withtyped/postgres": "^0.11.0", - "@withtyped/server": "^0.11.0", + "@withtyped/server": "^0.11.1", "accepts": "^1.3.8", "chalk": "^5.0.0", "decamelize": "^6.0.0", diff --git a/packages/cloud/src/libraries/tenants.ts b/packages/cloud/src/libraries/tenants.ts index 4e719f312..6d747668a 100644 --- a/packages/cloud/src/libraries/tenants.ts +++ b/packages/cloud/src/libraries/tenants.ts @@ -6,8 +6,6 @@ import { DemoConnector } from '@logto/connector-kit'; import { createTenantMetadata } from '@logto/core-kit'; import { type LogtoOidcConfigType, - type TenantInfo, - type CreateTenant, createAdminTenantApplicationRole, AdminTenantRole, createTenantMachineToMachineApplication, @@ -20,8 +18,9 @@ import { createAdminData, createAdminDataInAdminTenant, getManagementApiResourceIndicator, - type PatchTenant, + type TenantModel, } from '@logto/schemas'; +import type { TenantInfo } from '@logto/schemas/models'; import { generateStandardId } from '@logto/shared'; import { appendPath } from '@silverhand/essentials'; @@ -71,7 +70,10 @@ export class TenantsLibrary { })); } - async updateTenantById(tenantId: string, payload: PatchTenant): Promise { + async updateTenantById( + tenantId: string, + payload: Partial> + ): Promise { const { id, name, tag } = await this.queries.tenants.updateTenantById(tenantId, payload); return { id, name, tag, indicator: getManagementApiResourceIndicator(id) }; @@ -119,13 +121,13 @@ export class TenantsLibrary { async createNewTenant( forUserId: string, - payload: Pick + payload: Partial> ): Promise { const databaseName = await getDatabaseName(this.queries.client); const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName); // Init tenant - const createTenant: CreateTenant = { + const createTenant = { id: tenantId, dbUser: role, dbUserPassword: password, diff --git a/packages/cloud/src/queries/tenants.ts b/packages/cloud/src/queries/tenants.ts index 9bac9088f..50d072614 100644 --- a/packages/cloud/src/queries/tenants.ts +++ b/packages/cloud/src/queries/tenants.ts @@ -8,11 +8,9 @@ import { PredefinedScope, ApplicationType, type AdminData, - type CreateTenant, - type PatchTenant, type CreateRolesScope, - type TenantModel, } from '@logto/schemas'; +import type { TenantModel } from '@logto/schemas/models'; import { generateStandardId } from '@logto/shared'; import { type PostgreSql, @@ -47,20 +45,15 @@ export const createTenantsQueries = (client: Queryable) => { where roles.tenant_id = ${adminTenantId}; `); - const insertTenant = async (tenant: CreateTenant) => - client.query( - insertInto( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - Object.fromEntries(Object.entries(tenant).filter(([_, value]) => value !== undefined)), - 'tenants' - ) - ); + const insertTenant = async ( + tenant: Pick & + Partial> + ) => client.query(insertInto(tenant, 'tenants')); - const updateTenantById = async (tenantId: string, rawPayload: PatchTenant) => { - const payload: Record = Object.fromEntries( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - Object.entries(rawPayload).filter(([_, value]) => value !== undefined) - ); + const updateTenantById = async ( + tenantId: string, + payload: Partial> + ) => { const tenant = await client.maybeOne(sql` update tenants set ${Object.entries(payload).map(([key, value]) => sql`${id(key)}=${jsonIfNeeded(value)}`)} diff --git a/packages/cloud/src/routes/tenants.test.ts b/packages/cloud/src/routes/tenants.test.ts index e3ce8902e..9ad2aca0a 100644 --- a/packages/cloud/src/routes/tenants.test.ts +++ b/packages/cloud/src/routes/tenants.test.ts @@ -1,5 +1,5 @@ -import type { TenantInfo } from '@logto/schemas'; -import { CloudScope, TenantTag } from '@logto/schemas'; +import { CloudScope } from '@logto/schemas'; +import { type TenantInfo, TenantTag } from '@logto/schemas/models'; import { buildRequestAuthContext, createHttpContext } from '#src/test-utils/context.js'; import { noop } from '#src/test-utils/function.js'; diff --git a/packages/cloud/src/routes/tenants.ts b/packages/cloud/src/routes/tenants.ts index fec1cad87..023d63d8d 100644 --- a/packages/cloud/src/routes/tenants.ts +++ b/packages/cloud/src/routes/tenants.ts @@ -1,10 +1,5 @@ -import { - CloudScope, - tenantInfoGuard, - createTenantGuard, - adminTenantId, - defaultTenantId, -} from '@logto/schemas'; +import { CloudScope, adminTenantId, defaultTenantId } from '@logto/schemas'; +import { Tenants, tenantInfoGuard } from '@logto/schemas/models'; import { assert } from '@silverhand/essentials'; import { createRouter, RequestError } from '@withtyped/server'; @@ -23,7 +18,7 @@ export const tenantsRoutes = (library: TenantsLibrary) => .patch( '/:tenantId', { - body: createTenantGuard.pick({ name: true, tag: true }).partial(), + body: Tenants.guard('patch').pick({ name: true, tag: true }), response: tenantInfoGuard, }, async (context, next) => { @@ -61,7 +56,7 @@ export const tenantsRoutes = (library: TenantsLibrary) => .post( '/', { - body: createTenantGuard.pick({ name: true, tag: true }).required(), + body: Tenants.guard('create').pick({ name: true, tag: true }), response: tenantInfoGuard, }, async (context, next) => { diff --git a/packages/cloud/src/test-utils/libraries.ts b/packages/cloud/src/test-utils/libraries.ts index 2a9b1b315..2eb5d5803 100644 --- a/packages/cloud/src/test-utils/libraries.ts +++ b/packages/cloud/src/test-utils/libraries.ts @@ -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 { TenantsLibrary } from '#src/libraries/tenants.js'; diff --git a/packages/console/src/cloud/pages/Main/Redirect.tsx b/packages/console/src/cloud/pages/Main/Redirect.tsx index 7d078b5ad..eac7303ed 100644 --- a/packages/console/src/cloud/pages/Main/Redirect.tsx +++ b/packages/console/src/cloud/pages/Main/Redirect.tsx @@ -1,5 +1,5 @@ import { useLogto } from '@logto/react'; -import type { TenantInfo } from '@logto/schemas'; +import type { TenantInfo } from '@logto/schemas/models'; import { trySafe } from '@silverhand/essentials'; import { useContext, useEffect } from 'react'; import { useHref } from 'react-router-dom'; diff --git a/packages/console/src/cloud/pages/Main/Tenants.tsx b/packages/console/src/cloud/pages/Main/Tenants.tsx index a0a2b4ea4..e821f93c7 100644 --- a/packages/console/src/cloud/pages/Main/Tenants.tsx +++ b/packages/console/src/cloud/pages/Main/Tenants.tsx @@ -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 { useCloudApi } from '@/cloud/hooks/use-cloud-api'; diff --git a/packages/console/src/cloud/pages/Main/index.tsx b/packages/console/src/cloud/pages/Main/index.tsx index 70ed20bdb..03a594fdb 100644 --- a/packages/console/src/cloud/pages/Main/index.tsx +++ b/packages/console/src/cloud/pages/Main/index.tsx @@ -1,5 +1,5 @@ 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 { HTTPError } from 'ky'; import { useContext, useEffect, useState } from 'react'; diff --git a/packages/console/src/contexts/TenantsProvider.tsx b/packages/console/src/contexts/TenantsProvider.tsx index ec971bed8..7f8a2294b 100644 --- a/packages/console/src/contexts/TenantsProvider.tsx +++ b/packages/console/src/contexts/TenantsProvider.tsx @@ -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 type { ReactNode } from 'react'; import { useCallback, useMemo, createContext, useState } from 'react'; @@ -24,7 +25,7 @@ type Tenants = { const { tenantId, indicator } = defaultManagementApi.resource; const initialTenants = conditional( !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. ] ); diff --git a/packages/console/src/hooks/use-tenants.ts b/packages/console/src/hooks/use-tenants.ts index df9c234bc..0d04463b4 100644 --- a/packages/console/src/hooks/use-tenants.ts +++ b/packages/console/src/hooks/use-tenants.ts @@ -1,5 +1,5 @@ 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 ky from 'ky'; import { useCallback, useContext, useEffect, useMemo } from 'react'; diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/DeleteModal/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/DeleteModal/index.tsx index 52a0a11d5..7f0bedeb8 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/DeleteModal/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/DeleteModal/index.tsx @@ -1,4 +1,4 @@ -import { type TenantInfo } from '@logto/schemas'; +import { type TenantInfo } from '@logto/schemas/models'; import classNames from 'classnames'; import { useTranslation, Trans } from 'react-i18next'; diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx index 8d6ac8a8c..968a93878 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/ProfileForm/index.tsx @@ -1,5 +1,5 @@ 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 { useTranslation } from 'react-i18next'; diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx index 05670f46e..2bb59b260 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx @@ -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 { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; @@ -49,7 +49,7 @@ function TenantBasicSettings() { reset({ profile: { name, tag } }); }, [currentTenant, reset]); - const saveData = async (data: PatchTenant) => { + const saveData = async (data: { name?: string; tag?: TenantTag }) => { try { const { name, tag } = await cloudApi .patch(`/api/tenants/${currentTenantId}`, { diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/types.ts b/packages/console/src/pages/TenantSettings/TenantBasicSettings/types.ts index c95a81269..ac8279df0 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/types.ts +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/types.ts @@ -1,5 +1,5 @@ -import { type TenantInfo } from '@logto/schemas'; +import { type TenantModel } from '@logto/schemas/models'; export type TenantSettingsForm = { - profile: Pick; + profile: Pick; }; diff --git a/packages/integration-tests/src/api/tenant.ts b/packages/integration-tests/src/api/tenant.ts index 915926536..832ea345a 100644 --- a/packages/integration-tests/src/api/tenant.ts +++ b/packages/integration-tests/src/api/tenant.ts @@ -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'; export const createTenant = async ( accessToken: string, - payload: Required> + payload: { name: string; tag: TenantTag } ) => { return cloudApi .extend({ diff --git a/packages/integration-tests/src/tests/api-cloud/tenant.test.ts b/packages/integration-tests/src/tests/api-cloud/tenant.test.ts index eee3d39c0..85c3de0cc 100644 --- a/packages/integration-tests/src/tests/api-cloud/tenant.test.ts +++ b/packages/integration-tests/src/tests/api-cloud/tenant.test.ts @@ -11,11 +11,9 @@ import { type Resource, type Scope, type Role, - TenantTag, - type TenantInfo, - type CreateTenant, defaultTenantId, } from '@logto/schemas'; +import { TenantTag, type TenantInfo } from '@logto/schemas/models'; import { GlobalValues } from '@logto/shared'; import { appendPath } from '@silverhand/essentials'; @@ -40,7 +38,7 @@ describe('Tenant APIs', () => { for (const [payload, tenant] of [ [payload1, tenant1], [payload2, tenant2], - ] as Array<[Required>, TenantInfo]>) { + ] as Array<[{ name: string; tag: TenantTag }, TenantInfo]>) { expect(tenant).toHaveProperty('id'); expect(tenant).toHaveProperty('tag', payload.tag); expect(tenant).toHaveProperty('name', payload.name); @@ -82,7 +80,7 @@ describe('Tenant APIs', () => { [payload1, tenant1], [payload2, tenant2], [payload3, tenant3], - ] as Array<[Required>, TenantInfo]>) { + ] as Array<[{ name: string; tag: TenantTag }, TenantInfo]>) { expect(tenant).toHaveProperty('id'); expect(tenant).toHaveProperty('tag', payload.tag); expect(tenant).toHaveProperty('name', payload.name); diff --git a/packages/schemas/package.json b/packages/schemas/package.json index a212964be..d5d8c5117 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -86,7 +86,7 @@ "@logto/phrases": "workspace:^1.4.0", "@logto/phrases-ui": "workspace:^1.2.0", "@logto/shared": "workspace:^2.0.0", - "@withtyped/server": "^0.11.0", + "@withtyped/server": "^0.11.1", "zod": "^3.20.2" } } diff --git a/packages/schemas/src/models/tenants.ts b/packages/schemas/src/models/tenants.ts index 08643bfab..b8dc9a967 100644 --- a/packages/schemas/src/models/tenants.ts +++ b/packages/schemas/src/models/tenants.ts @@ -1,6 +1,12 @@ 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 */ ` /* init_order = 0 */ @@ -16,4 +22,14 @@ export const Tenants = createModel(/* sql */ ` unique (db_user) ); /* no_after_each */ -`); +`) + .extend('tag', z.nativeEnum(TenantTag)) + .extend('createdAt', { readonly: true }); + +export type TenantModel = InferModelType; + +export type TenantInfo = Pick & { indicator: string }; + +export const tenantInfoGuard: z.ZodType = Tenants.guard('model') + .pick({ id: true, name: true, tag: true }) + .extend({ indicator: z.string() }); diff --git a/packages/schemas/src/seeds/tenant.ts b/packages/schemas/src/seeds/tenant.ts index 51a3f4ecd..5f18eba03 100644 --- a/packages/schemas/src/seeds/tenant.ts +++ b/packages/schemas/src/seeds/tenant.ts @@ -1,8 +1,4 @@ -import type { InferModelType } from '@withtyped/server/model'; - -import type { Tenants } from '../models/tenants.js'; +export type { TenantModel } from '../models/tenants.js'; export const defaultTenantId = 'default'; export const adminTenantId = 'admin'; - -export type TenantModel = InferModelType; diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index c5cd7a3b9..d047a7486 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -11,7 +11,6 @@ export * from './role.js'; export * from './verification-code.js'; export * from './application.js'; export * from './system.js'; -export * from './tenant.js'; export * from './user-assets.js'; export * from './hook.js'; export * from './service-log.js'; diff --git a/packages/schemas/src/types/tenant.ts b/packages/schemas/src/types/tenant.ts deleted file mode 100644 index 984e025ca..000000000 --- a/packages/schemas/src/types/tenant.ts +++ /dev/null @@ -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>; -export type CreateTenant = Pick & - 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 & { indicator: string }; - -export const tenantInfoGuard: z.ZodType = createTenantGuard - .pick({ id: true, name: true, tag: true }) - .extend({ indicator: z.string() }) - .required(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e5e1757e..167ffc98e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,10 +253,10 @@ importers: version: 2.5.0 '@withtyped/postgres': specifier: ^0.11.0 - version: 0.11.0(@withtyped/server@0.11.0) + version: 0.11.0(@withtyped/server@0.11.1) '@withtyped/server': - specifier: ^0.11.0 - version: 0.11.0(zod@3.20.2) + specifier: ^0.11.1 + version: 0.11.1(zod@3.20.2) accepts: specifier: ^1.3.8 version: 1.3.8 @@ -3556,8 +3556,8 @@ importers: specifier: workspace:^2.0.0 version: link:../shared '@withtyped/server': - specifier: ^0.11.0 - version: 0.11.0(zod@3.20.2) + specifier: ^0.11.1 + version: 0.11.1(zod@3.20.2) zod: specifier: ^3.20.2 version: 3.20.2 @@ -9808,21 +9808,21 @@ packages: eslint-visitor-keys: 3.3.0 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==} peerDependencies: '@withtyped/server': ^0.11.0 dependencies: '@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 pg: 8.8.0 transitivePeerDependencies: - pg-native dev: false - /@withtyped/server@0.11.0(zod@3.20.2): - resolution: {integrity: sha512-InF3g9uv63ImK/sTIywcwX9tg2KnyFOlYsqeVI1WDjV1eTdzVxRJInD8iA08+U10S/dhBimukSx2Bfjwt8MWiw==} + /@withtyped/server@0.11.1(zod@3.20.2): + resolution: {integrity: sha512-9b/rH+6S8rKlgVTuDfDOSXiVjOBz5yd4EIP6qswncIJvA+i9j1ykR0GIBGuJSQAr7SWrSNCILUVK+oDTiWzJ6g==} peerDependencies: zod: ^3.19.1 dependencies: