0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(cloud): POST /tenants

This commit is contained in:
Gao Sun 2023-02-20 00:27:12 +08:00
parent 12377665b4
commit 76a04d97b3
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
26 changed files with 350 additions and 99 deletions

View file

@ -10,6 +10,8 @@ echo Install production dependencies
NODE_ENV=production pnpm i
echo Prune files
# Remove cloud in OSS distributions
rm -rf packages/cloud
# Some node packages use `src` as their dist folder, so ignore them from the rm list in the end
find \
.git .changeset .changeset-staged .devcontainer .github .husky .parcel-cache .scripts .vscode pnpm-*.yaml *.js \

View file

@ -1,7 +1,6 @@
import { readdir, readFile } from 'fs/promises';
import path from 'path';
import { generateStandardId } from '@logto/core-kit';
import {
defaultSignInExperience,
createDefaultAdminConsoleConfig,
@ -9,7 +8,7 @@ import {
defaultTenantId,
adminTenantId,
defaultManagementApi,
createManagementApiInAdminTenant,
createAdminDataInAdminTenant,
createMeApiInAdminTenant,
} from '@logto/schemas';
import { Hooks, Tenants } from '@logto/schemas/models';
@ -120,13 +119,14 @@ export const seedTables = async (
await createTenant(connection, adminTenantId);
await seedOidcConfigs(connection, adminTenantId);
await seedAdminData(connection, createManagementApiInAdminTenant(defaultTenantId));
await seedAdminData(connection, createAdminDataInAdminTenant(defaultTenantId));
await seedAdminData(connection, createMeApiInAdminTenant());
await Promise.all([
connection.query(insertInto(createDefaultAdminConsoleConfig(), 'logto_configs')),
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
connection.query(insertInto(createDemoAppApplication(generateStandardId()), 'applications')),
// TODO: @gao remove demo app
connection.query(insertInto(createDemoAppApplication(defaultTenantId), 'applications')),
updateDatabaseTimestamp(connection, latestTimestamp),
]);
};

View file

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

View file

@ -19,12 +19,14 @@
"start": "NODE_ENV=production node build/index.js"
},
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@withtyped/postgres": "^0.7.0",
"@withtyped/server": "^0.7.0",
"@withtyped/postgres": "^0.8.0",
"@withtyped/server": "^0.8.0",
"chalk": "^5.0.0",
"decamelize": "^6.0.0",
"dotenv": "^16.0.0",
"find-up": "^6.3.0",
"http-proxy": "^1.18.1",

View file

@ -0,0 +1,79 @@
import { generateStandardId } from '@logto/core-kit';
import type { TenantModel } from '@logto/schemas';
import {
LogtoConfigs,
SignInExperiences,
createDefaultAdminConsoleConfig,
createDefaultSignInExperience,
adminTenantId,
createAdminData,
createAdminDataInAdminTenant,
} from '@logto/schemas';
import { createTenantMetadata } from '@logto/shared';
import type { ZodType } from 'zod';
import { z } from 'zod';
import type { Queries } from '#src/queries/index.js';
import { getDatabaseName } from '#src/queries/utils.js';
import { insertInto } from '#src/utils/query.js';
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
export type TenantInfo = {
id: string;
indicator: string;
};
export const tenantInfoGuard: ZodType<TenantInfo> = z.object({
id: z.string(),
indicator: z.string(),
});
export const createTenantsLibrary = (queries: Queries) => {
const { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData } =
queries.tenants;
const { assignRoleToUser } = queries.users;
const getAvailableTenants = async (userId: string): Promise<TenantInfo[]> => {
const { rows } = await getManagementApiLikeIndicatorsForUser(userId);
return rows
.map(({ indicator }) => ({
id: getTenantIdFromManagementApiIndicator(indicator),
indicator,
}))
.filter((tenant): tenant is TenantInfo => Boolean(tenant.id));
};
const createNewTenant = async (forUserId: string): Promise<TenantInfo> => {
const { client } = queries;
const databaseName = await getDatabaseName(client);
const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName);
// TODO: @gao wrap into transaction
// Init tenant
const tenantModel: TenantModel = { id: tenantId, dbUser: role, dbUserPassword: password };
await insertTenant(tenantModel);
await createTenantRole(parentRole, role, password);
// Create admin data set (resource, roles, etc.)
const adminDataInAdminTenant = createAdminDataInAdminTenant(tenantId);
await insertAdminData(adminDataInAdminTenant);
await insertAdminData(createAdminData(tenantId));
await assignRoleToUser({
id: generateStandardId(),
tenantId: adminTenantId,
userId: forUserId,
roleId: adminDataInAdminTenant.role.id,
});
// Create initial configs
await Promise.all([
client.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)),
client.query(insertInto(createDefaultSignInExperience(tenantId), SignInExperiences.table)),
]);
return { id: tenantId, indicator: adminDataInAdminTenant.resource.indicator };
};
return { getAvailableTenants, createNewTenant };
};

View file

@ -2,6 +2,7 @@ import assert from 'node:assert';
import type { IncomingHttpHeaders } from 'node:http';
import path from 'node:path/posix';
import { tryThat } from '@logto/shared';
import type { NextFunction, RequestContext } from '@withtyped/server';
import { RequestError } from '@withtyped/server';
import { createRemoteJWKSet, jwtVerify } from 'jose';
@ -57,31 +58,26 @@ export default function withAuth<InputContext extends RequestContext>({
return async (context: InputContext, next: NextFunction<WithAuthContext<InputContext>>) => {
const [getKey, issuer] = await getJwkSet;
try {
const {
payload: { sub, scope },
} = await jwtVerify(extractBearerTokenFromHeaders(context.request.headers), getKey, {
const {
payload: { sub, scope },
} = await tryThat(
jwtVerify(extractBearerTokenFromHeaders(context.request.headers), getKey, {
issuer,
audience,
});
assert(sub, new RequestError('"sub" is missing in JWT.', 401));
const scopes = typeof scope === 'string' ? scope.split(' ') : [];
assert(
expectScopes.every((scope) => scopes.includes(scope)),
new RequestError('Forbidden. Please check your permissions.', 403)
);
await next({ ...context, auth: { id: sub, scopes } });
return;
} catch (error: unknown) {
if (error instanceof RequestError) {
}),
(error) => {
throw error;
}
);
throw new RequestError('Unauthorized.', 401);
}
assert(sub, new RequestError('"sub" is missing in JWT.', 401));
const scopes = typeof scope === 'string' ? scope.split(' ') : [];
assert(
expectScopes.every((scope) => scopes.includes(scope)),
new RequestError('Forbidden. Please check your permissions.', 403)
);
return next({ ...context, auth: { id: sub, scopes } });
};
}

View file

@ -3,4 +3,13 @@ import { createQueryClient } from '@withtyped/postgres';
import { EnvSet } from '#src/env-set/index.js';
import { parseDsn } from '#src/utils/postgres.js';
export const client = createQueryClient(parseDsn(EnvSet.global.dbUrl));
import { createTenantsQueries } from './tenants.js';
import { createUsersQueries } from './users.js';
export class Queries {
static default = new Queries();
public readonly client = createQueryClient(parseDsn(EnvSet.global.dbUrl));
public readonly tenants = createTenantsQueries(this.client);
public readonly users = createUsersQueries(this.client);
}

View file

@ -1,6 +1,19 @@
import { adminTenantId, getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas';
import assert from 'node:assert';
import { generateStandardId } from '@logto/core-kit';
import type { AdminData, TenantModel } from '@logto/schemas';
import {
adminTenantId,
getManagementApiResourceIndicator,
PredefinedScope,
CreateRolesScope,
} from '@logto/schemas';
import type { PostgresQueryClient } from '@withtyped/postgres';
import { sql } from '@withtyped/postgres';
import { dangerousRaw, id, sql } from '@withtyped/postgres';
import { insertInto } from '#src/utils/query.js';
export type TenantsQueries = ReturnType<typeof createTenantsQueries>;
export const createTenantsQueries = (client: PostgresQueryClient) => {
const getManagementApiLikeIndicatorsForUser = async (userId: string) =>
@ -20,5 +33,43 @@ export const createTenantsQueries = (client: PostgresQueryClient) => {
where roles.tenant_id = ${adminTenantId};
`);
return { getManagementApiLikeIndicatorsForUser };
const insertTenant = async (tenant: TenantModel) => client.query(insertInto(tenant, 'tenants'));
const createTenantRole = async (parentRole: string, role: string, password: string) =>
client.query(sql`
create role ${id(role)} with inherit login
password '${dangerousRaw(password)}'
in role ${id(parentRole)};
`);
const insertAdminData = async (data: AdminData) => {
const { resource, scope, role } = data;
assert(
resource.tenantId && scope.tenantId && role.tenantId,
new Error('Tenant ID cannot be empty.')
);
assert(
resource.tenantId === scope.tenantId && scope.tenantId === role.tenantId,
new Error('All data should have the same tenant ID.')
);
await client.query(insertInto(resource, 'resources'));
await client.query(insertInto(scope, 'scopes'));
await client.query(insertInto(role, 'roles'));
await client.query(
insertInto(
{
id: generateStandardId(),
roleId: role.id,
scopeId: scope.id,
tenantId: resource.tenantId,
} satisfies CreateRolesScope,
'roles_scopes'
)
);
};
return { getManagementApiLikeIndicatorsForUser, insertTenant, createTenantRole, insertAdminData };
};

View file

@ -0,0 +1,12 @@
import type { UsersRole } from '@logto/schemas';
import type { PostgresQueryClient } from '@withtyped/postgres';
import { insertInto } from '#src/utils/query.js';
export type UsersQueries = ReturnType<typeof createUsersQueries>;
export const createUsersQueries = (client: PostgresQueryClient) => {
const assignRoleToUser = async (data: UsersRole) => client.query(insertInto(data, 'users_roles'));
return { assignRoleToUser };
};

View file

@ -0,0 +1,17 @@
import type { PostgresQueryClient } from '@withtyped/postgres';
import { sql } from '@withtyped/postgres';
import { RequestError } from '@withtyped/server';
export const getDatabaseName = async (client: PostgresQueryClient) => {
const {
rows: [first],
} = await client.query<{ currentDatabase: string }>(sql`
select current_database() as "currentDatabase";
`);
if (!first) {
throw new RequestError(undefined, 500);
}
return first.currentDatabase.replaceAll('-', '_');
};

View file

@ -1,26 +1,21 @@
import { createRouter } from '@withtyped/server';
import { z } from 'zod';
import { createRouter, RequestError } from '@withtyped/server';
import { createTenantsLibrary, tenantInfoGuard } from '#src/libraries/tenants.js';
import type { WithAuthContext } from '#src/middleware/with-auth.js';
import { client } from '#src/queries/index.js';
import { createTenantsQueries } from '#src/queries/tenants.js';
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
import { Queries } from '#src/queries/index.js';
const { getManagementApiLikeIndicatorsForUser } = createTenantsQueries(client);
const { getAvailableTenants, createNewTenant } = createTenantsLibrary(Queries.default);
export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants').get(
'/',
{ response: z.object({ id: z.string(), indicator: z.string() }).array() },
async (context, next) => {
const { rows } = await getManagementApiLikeIndicatorsForUser(context.auth.id);
export const tenants = createRouter<WithAuthContext, '/tenants'>('/tenants')
.get('/', { response: tenantInfoGuard.array() }, async (context, next) => {
return next({ ...context, json: await getAvailableTenants(context.auth.id) });
})
.post('/', { response: tenantInfoGuard }, async (context, next) => {
const tenants = await getAvailableTenants(context.auth.id);
const tenants = rows
.map(({ indicator }) => ({
id: getTenantIdFromManagementApiIndicator(indicator),
indicator,
}))
.filter((tenant): tenant is { id: string; indicator: string } => Boolean(tenant.id));
if (tenants.length > 0) {
throw new RequestError('The user already has a tenant.', 409);
}
return next({ ...context, json: tenants });
}
);
return next({ ...context, json: await createNewTenant(context.auth.id) });
});

View file

@ -0,0 +1,13 @@
import { id, jsonIfNeeded, sql } from '@withtyped/postgres';
import type { JsonObject } from '@withtyped/server';
import decamelize from 'decamelize';
export const insertInto = <T extends JsonObject>(object: T, table: string) => {
const entries = Object.entries(object);
return sql`
insert into ${id(table)}
(${entries.map(([key]) => id(decamelize(key)))})
values (${entries.map(([, value]) => jsonIfNeeded(value))})
`;
};

View file

@ -35,8 +35,8 @@
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",
"@silverhand/essentials": "2.2.0",
"@withtyped/postgres": "^0.7.0",
"@withtyped/server": "^0.7.0",
"@withtyped/postgres": "^0.8.0",
"@withtyped/server": "^0.8.0",
"chalk": "^5.0.0",
"clean-deep": "^3.4.0",
"date-fns": "^2.29.3",

View file

@ -1,8 +1,13 @@
import type { PostgreSql } from '@withtyped/postgres';
import type { Transaction } from '@withtyped/server';
import { QueryClient } from '@withtyped/server';
// Consider move to withtyped if everything goes well
export class MockQueryClient extends QueryClient<PostgreSql> {
async transaction(): Promise<Transaction<PostgreSql>> {
throw new Error('Method not implemented.');
}
async connect() {
console.debug('MockQueryClient connect');
}

View file

@ -53,6 +53,6 @@
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {
"@withtyped/server": "^0.7.0"
"@withtyped/server": "^0.8.0"
}
}

View file

@ -84,7 +84,7 @@
"@logto/language-kit": "workspace:*",
"@logto/phrases": "workspace:*",
"@logto/phrases-ui": "workspace:*",
"@withtyped/server": "^0.7.0",
"@withtyped/server": "^0.8.0",
"zod": "^3.20.2"
}
}

View file

@ -1,6 +1,7 @@
import { generateStandardId } from '@logto/core-kit';
import type { CreateApplication } from '../db-entries/index.js';
import { ApplicationType } from '../db-entries/index.js';
import { defaultTenantId } from './tenant.js';
/**
* The fixed application ID for Admin Console.
@ -11,10 +12,11 @@ export const adminConsoleApplicationId = 'admin-console';
export const demoAppApplicationId = 'demo-app';
export const createDemoAppApplication = (secret: string): Readonly<CreateApplication> => ({
tenantId: defaultTenantId,
/** @deprecated Demo app database entity will be removed soon. */
export const createDemoAppApplication = (forTenantId: string): Readonly<CreateApplication> => ({
tenantId: forTenantId,
id: demoAppApplicationId,
secret,
secret: generateStandardId(),
name: 'Demo App',
description: 'Logto demo app.',
type: ApplicationType.SPA,

View file

@ -2,14 +2,15 @@ import { CreateLogtoConfig } from '../db-entries/index.js';
import { AppearanceMode } from '../foundations/index.js';
import type { AdminConsoleData } from '../types/index.js';
import { AdminConsoleConfigKey } from '../types/index.js';
import { defaultTenantId } from './tenant.js';
export const createDefaultAdminConsoleConfig = (): Readonly<{
export const createDefaultAdminConsoleConfig = (
forTenantId: string
): Readonly<{
key: AdminConsoleConfigKey;
value: AdminConsoleData;
}> =>
Object.freeze({
tenantId: defaultTenantId,
tenantId: forTenantId,
key: AdminConsoleConfigKey.AdminConsole,
value: {
language: 'en',

View file

@ -14,7 +14,7 @@ export type AdminData = {
const defaultResourceId = 'management-api';
const defaultScopeAllId = 'management-api-all';
// Consider combine this with `createManagementApiInAdminTenant()`
// Consider combine this with `createAdminData()`
/** The fixed Management API Resource for `default` tenant. */
export const defaultManagementApi = Object.freeze({
resource: {
@ -47,13 +47,50 @@ export const defaultManagementApi = Object.freeze({
},
}) satisfies AdminData;
export const getManagementApiResourceIndicator = (tenantId: string, path = 'api') =>
`https://${tenantId}.logto.app/${path}`;
export function getManagementApiResourceIndicator<TenantId extends string>(
tenantId: TenantId
): `https://${TenantId}.logto.app/api`;
export function getManagementApiResourceIndicator<TenantId extends string, Path extends string>(
tenantId: TenantId,
path: Path
): `https://${TenantId}.logto.app/${Path}`;
export const getManagementApiAdminName = (tenantId: string) => `${tenantId}:${UserRole.Admin}`;
export function getManagementApiResourceIndicator(tenantId: string, path = 'api') {
return `https://${tenantId}.logto.app/${path}`;
}
/** Create a Management API Resource of the given tenant ID for `admin` tenant. */
export const createManagementApiInAdminTenant = (tenantId: string): AdminData => {
export const getManagementApiAdminName = <TenantId extends string>(tenantId: TenantId) =>
`${tenantId}:${UserRole.Admin}` as const;
/** Create a set of admin data for Management API of the given tenant ID. */
export const createAdminData = (tenantId: string): AdminData => {
const resourceId = generateStandardId();
return Object.freeze({
resource: {
tenantId,
id: resourceId,
indicator: getManagementApiResourceIndicator(tenantId),
name: `Logto Management API`,
},
scope: {
tenantId,
id: generateStandardId(),
name: PredefinedScope.All,
description: 'Default scope for Management API, allows all permissions.',
resourceId,
},
role: {
tenantId,
id: generateStandardId(),
name: UserRole.Admin,
description: 'Admin role for Logto.',
},
});
};
/** Create a set of admin data for Management API of the given tenant ID for `admin` tenant. */
export const createAdminDataInAdminTenant = (tenantId: string): AdminData => {
const resourceId = generateStandardId();
return Object.freeze({

View file

@ -7,8 +7,10 @@ import { defaultTenantId } from './tenant.js';
const defaultPrimaryColor = '#6139F6';
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
tenantId: defaultTenantId,
export const createDefaultSignInExperience = (
forTenantId: string
): Readonly<CreateSignInExperience> => ({
tenantId: forTenantId,
id: 'default',
color: {
primaryColor: defaultPrimaryColor,
@ -42,7 +44,10 @@ export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
},
socialSignInConnectorTargets: [],
signInMode: SignInMode.SignInAndRegister,
};
});
/** @deprecated Use `createDefaultSignInExperience()` instead. */
export const defaultSignInExperience = createDefaultSignInExperience(defaultTenantId);
export const adminConsoleSignInExperience: CreateSignInExperience = {
...defaultSignInExperience,

View file

@ -12,8 +12,5 @@ create table sign_in_experiences (
sign_up jsonb /* @use SignUp */ not null,
social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb,
sign_in_mode sign_in_mode not null default 'SignInAndRegister',
primary key (id)
primary key (tenant_id, id)
);
create index sign_in_experiences__id
on sign_in_experiences (tenant_id, id);

View file

@ -33,7 +33,6 @@
},
"devDependencies": {
"@logto/connector-kit": "workspace:*",
"@logto/core-kit": "workspace:*",
"@silverhand/eslint-config": "2.0.1",
"@silverhand/ts-config": "2.0.3",
"@types/jest": "^29.1.2",
@ -55,6 +54,7 @@
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@silverhand/essentials": "2.2.0",
"find-up": "^6.3.0",

View file

@ -1,2 +1,3 @@
export * from './database/index.js';
export * from './utils/index.js';
export * from './models/index.js';

View file

@ -0,0 +1 @@
export * from './tenant.js';

View file

@ -0,0 +1,23 @@
import { generateStandardId } from '@logto/core-kit';
import { customAlphabet } from 'nanoid';
// Use lowercase letters for tenant IDs to improve compatibility
export const tenantIdAlphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
export type TenantMetadata = {
id: string;
parentRole: string;
role: string;
password: string;
};
export const createTenantMetadata = (
databaseName: string,
tenantId = customAlphabet(tenantIdAlphabet)(6)
): TenantMetadata => {
const parentRole = `logto_tenant_${databaseName}`;
const role = `logto_tenant_${databaseName}_${tenantId}`;
const password = generateStandardId(32);
return { id: tenantId, parentRole, role, password };
};

View file

@ -106,6 +106,7 @@ importers:
packages/cloud:
specifiers:
'@logto/core-kit': workspace:*
'@logto/schemas': workspace:*
'@logto/shared': workspace:*
'@silverhand/eslint-config': 2.0.1
@ -114,9 +115,10 @@ importers:
'@types/http-proxy': ^1.17.9
'@types/mime-types': ^2.1.1
'@types/node': ^18.11.18
'@withtyped/postgres': ^0.7.0
'@withtyped/server': ^0.7.0
'@withtyped/postgres': ^0.8.0
'@withtyped/server': ^0.8.0
chalk: ^5.0.0
decamelize: ^6.0.0
dotenv: ^16.0.0
eslint: ^8.21.0
find-up: ^6.3.0
@ -129,12 +131,14 @@ importers:
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
'@silverhand/essentials': 2.2.0
'@withtyped/postgres': 0.7.0_@withtyped+server@0.7.0
'@withtyped/server': 0.7.0
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
'@withtyped/server': 0.8.0
chalk: 5.1.2
decamelize: 6.0.0
dotenv: 16.0.0
find-up: 6.3.0
http-proxy: 1.18.1
@ -331,8 +335,8 @@ importers:
'@types/semver': ^7.3.12
'@types/sinon': ^10.0.13
'@types/supertest': ^2.0.11
'@withtyped/postgres': ^0.7.0
'@withtyped/server': ^0.7.0
'@withtyped/postgres': ^0.8.0
'@withtyped/server': ^0.8.0
chalk: ^5.0.0
clean-deep: ^3.4.0
copyfiles: ^2.4.1
@ -391,8 +395,8 @@ importers:
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
'@silverhand/essentials': 2.2.0
'@withtyped/postgres': 0.7.0_@withtyped+server@0.7.0
'@withtyped/server': 0.7.0
'@withtyped/postgres': 0.8.0_@withtyped+server@0.8.0
'@withtyped/server': 0.8.0
chalk: 5.1.2
clean-deep: 3.4.0
date-fns: 2.29.3
@ -540,7 +544,7 @@ importers:
'@types/jest': ^29.1.2
'@types/jest-environment-puppeteer': ^5.0.2
'@types/node': ^18.11.18
'@withtyped/server': ^0.7.0
'@withtyped/server': ^0.8.0
dotenv: ^16.0.0
eslint: ^8.34.0
got: ^12.5.3
@ -554,7 +558,7 @@ importers:
text-encoder: ^0.0.4
typescript: ^4.9.4
dependencies:
'@withtyped/server': 0.7.0
'@withtyped/server': 0.8.0
devDependencies:
'@jest/types': 29.1.2
'@logto/connector-kit': link:../toolkit/connector-kit
@ -647,7 +651,7 @@ importers:
'@types/jest': ^29.1.2
'@types/node': ^18.11.18
'@types/pluralize': ^0.0.29
'@withtyped/server': ^0.7.0
'@withtyped/server': ^0.8.0
camelcase: ^7.0.0
eslint: ^8.34.0
jest: ^29.1.2
@ -665,7 +669,7 @@ importers:
'@logto/language-kit': link:../toolkit/language-kit
'@logto/phrases': link:../phrases
'@logto/phrases-ui': link:../phrases-ui
'@withtyped/server': 0.7.0
'@withtyped/server': 0.8.0
zod: 3.20.2
devDependencies:
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
@ -705,6 +709,7 @@ importers:
slonik: ^30.0.0
typescript: ^4.9.4
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@silverhand/essentials': 2.2.0
find-up: 6.3.0
@ -712,7 +717,6 @@ importers:
slonik: 30.1.2
devDependencies:
'@logto/connector-kit': link:../toolkit/connector-kit
'@logto/core-kit': link:../toolkit/core-kit
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@types/jest': 29.1.2
@ -4592,21 +4596,21 @@ packages:
eslint-visitor-keys: 3.3.0
dev: true
/@withtyped/postgres/0.7.0_@withtyped+server@0.7.0:
resolution: {integrity: sha512-D6bI+ols0mtNvaTUp4IzNHAQtbqdakNTZgQ0E0KbVMvdfq0fhOFUaTKANRPjMs4rsyfAETfPzjt3B/Ij47ZiMA==}
/@withtyped/postgres/0.8.0_@withtyped+server@0.8.0:
resolution: {integrity: sha512-cyhHR1lEV1cs0yDfmHvqR4R52kxgI/JCDd2fizOCzgIE6Z9g3GjYjeVtTl/fTaoDz/QDaXKwfkuxd0N7HSY7uw==}
peerDependencies:
'@withtyped/server': ^0.7.0
'@withtyped/server': ^0.8.0
dependencies:
'@types/pg': 8.6.6
'@withtyped/server': 0.7.0
'@withtyped/server': 0.8.0
'@withtyped/shared': 0.2.0
pg: 8.8.0
transitivePeerDependencies:
- pg-native
dev: false
/@withtyped/server/0.7.0:
resolution: {integrity: sha512-UVW6cOJyOBDfGiSoMg2asYsKqmzL7+UPaYwqW+oxZtvlUabaCekVKTBH3l8tm7zhiOluZg9FD78t0DVuEyQTMw==}
/@withtyped/server/0.8.0:
resolution: {integrity: sha512-p9gRdEvUNBJ0X15jB4xZutMkzjF19EoBrGjvmaokbuO4+Ub5CSpfV/SOl+7vob8cAgIdWVriZfDGD73ZH0YWJQ==}
dependencies:
'@withtyped/shared': 0.2.0
dev: false