From e22eb3a9d20ee45b68f6578405004a1476c4a5f1 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 16 Mar 2024 00:50:10 +0800 Subject: [PATCH] refactor: remove slonik from shared --- .../cli/src/commands/database/seed/tables.ts | 3 +- packages/cli/src/database.ts | 2 +- packages/cli/src/queries/logto-config.ts | 3 +- packages/cli/src/queries/system.test.ts | 2 +- packages/cli/src/queries/system.ts | 3 +- packages/cli/src/sql.ts | 75 +++++++++++++++++++ .../database => core/src/utils}/sql.test.ts | 2 +- .../src/database => core/src/utils}/sql.ts | 24 ++---- packages/shared/package.json | 3 +- packages/shared/src/database/types.ts | 5 -- packages/shared/src/index.ts | 1 - pnpm-lock.yaml | 3 - 12 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 packages/cli/src/sql.ts rename packages/{shared/src/database => core/src/utils}/sql.test.ts (98%) rename packages/{shared/src/database => core/src/utils}/sql.ts (80%) diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index 1135e0f48..05430a277 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -28,7 +28,7 @@ import { } from '@logto/schemas'; import { getTenantRole } from '@logto/schemas'; import { Tenants } from '@logto/schemas/models'; -import { convertToIdentifiers, generateStandardId } from '@logto/shared'; +import { generateStandardId } from '@logto/shared'; import type { DatabaseTransactionConnection } from 'slonik'; import { sql } from 'slonik'; import { raw } from 'slonik-sql-tag-raw'; @@ -36,6 +36,7 @@ import { raw } from 'slonik-sql-tag-raw'; import { insertInto } from '../../../database.js'; import { getDatabaseName } from '../../../queries/database.js'; import { updateDatabaseTimestamp } from '../../../queries/system.js'; +import { convertToIdentifiers } from '../../../sql.js'; import { consoleLog, getPathInModule } from '../../../utils.js'; import { appendAdminConsoleRedirectUris, seedTenantCloudServiceApplication } from './cloud.js'; diff --git a/packages/cli/src/database.ts b/packages/cli/src/database.ts index 26ca45a73..b68007f8d 100644 --- a/packages/cli/src/database.ts +++ b/packages/cli/src/database.ts @@ -1,11 +1,11 @@ import type { SchemaLike } from '@logto/schemas'; -import { convertToPrimitiveOrSql } from '@logto/shared'; import { assert } from '@silverhand/essentials'; import decamelize from 'decamelize'; import { DatabaseError } from 'pg-protocol'; import { createPool, parseDsn, sql, stringifyDsn } from 'slonik'; import { createInterceptors } from 'slonik-interceptor-preset'; +import { convertToPrimitiveOrSql } from './sql.js'; import { ConfigKey, consoleLog, getCliConfigWithPrompt } from './utils.js'; export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto'; diff --git a/packages/cli/src/queries/logto-config.ts b/packages/cli/src/queries/logto-config.ts index a0912711b..e8e0f1fcd 100644 --- a/packages/cli/src/queries/logto-config.ts +++ b/packages/cli/src/queries/logto-config.ts @@ -1,11 +1,12 @@ import type { LogtoConfig, LogtoConfigKey, logtoConfigGuards } from '@logto/schemas'; import { LogtoConfigs } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; import type { CommonQueryMethods } from 'slonik'; import { sql } from 'slonik'; import type { z } from 'zod'; +import { convertToIdentifiers } from '../sql.js'; + const { table, fields } = convertToIdentifiers(LogtoConfigs); export const doesConfigsTableExist = async (pool: CommonQueryMethods) => { diff --git a/packages/cli/src/queries/system.test.ts b/packages/cli/src/queries/system.test.ts index 8eda604e2..6d77c540f 100644 --- a/packages/cli/src/queries/system.test.ts +++ b/packages/cli/src/queries/system.test.ts @@ -1,8 +1,8 @@ import { AlterationStateKey, Systems } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; import { DatabaseError } from 'pg-protocol'; import { createMockPool, createMockQueryResult, sql } from 'slonik'; +import { convertToIdentifiers } from '../sql.js'; import type { QueryType } from '../test-utils.js'; import { expectSqlAssert } from '../test-utils.js'; diff --git a/packages/cli/src/queries/system.ts b/packages/cli/src/queries/system.ts index 9b54907eb..1e278614b 100644 --- a/packages/cli/src/queries/system.ts +++ b/packages/cli/src/queries/system.ts @@ -1,12 +1,13 @@ import type { AlterationState, System, SystemKey } from '@logto/schemas'; import { systemGuards, Systems, AlterationStateKey } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; import { DatabaseError } from 'pg-protocol'; import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik'; import { sql } from 'slonik'; import type { z } from 'zod'; +import { convertToIdentifiers } from '../sql.js'; + const { fields, table } = convertToIdentifiers(Systems); const doesTableExist = async (pool: CommonQueryMethods, table: string) => { diff --git a/packages/cli/src/sql.ts b/packages/cli/src/sql.ts new file mode 100644 index 000000000..b02bcca83 --- /dev/null +++ b/packages/cli/src/sql.ts @@ -0,0 +1,75 @@ +/** + * @fileoverview Copied from `@logto/core`. Originally we put them in `@logto/shared` but it + * requires `slonik` which makes the package too heavy. + * + * Since `@logto/cli` only use these functions in a stable manner, we copy them here for now. If + * the number of functions grows, we should consider moving them to a separate package. (Actually, + * we should remove the dependency on `slonik` at all, and this may not be an issue then.) + */ + +import { type SchemaValue, type SchemaValuePrimitive, type Table } from '@logto/shared'; +import { type IdentifierSqlToken, type SqlToken, sql } from 'slonik'; + +/** + * Note `undefined` is removed from the acceptable list, + * since you should NOT call this function if ignoring the field is the desired behavior. + * Calling this function with `null` means an explicit `null` setting in database is expected. + * @param key The key of value. Will treat as `timestamp` if it ends with `_at` or 'At' AND value is a number; + * @param value The value to convert. + * @returns A primitive that can be saved into database. + */ +export const convertToPrimitiveOrSql = ( + key: string, + value: SchemaValue + // eslint-disable-next-line @typescript-eslint/ban-types +): NonNullable | SqlToken | null => { + if (value === null) { + return null; + } + + if (typeof value === 'object') { + return JSON.stringify(value); + } + + if ( + (['_at', 'At'].some((value) => key.endsWith(value)) || key === 'date') && + typeof value === 'number' + ) { + return sql`to_timestamp(${value}::double precision / 1000)`; + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + if (value === '') { + return null; + } + + return value; + } + + throw new Error(`Cannot convert ${key} to primitive`); +}; + +type FieldIdentifiers = { + [key in Key]: IdentifierSqlToken; +}; + +export const convertToIdentifiers = ( + { table, fields }: Table, + withPrefix = false +) => { + const fieldsIdentifiers = Object.entries(fields).map<[Key, IdentifierSqlToken]>( + // eslint-disable-next-line no-restricted-syntax -- Object.entries can only return string keys + ([key, value]) => [key as Key, sql.identifier(withPrefix ? [table, value] : [value])] + ); + + return { + table: sql.identifier([table]), + // Key value inferred from the original fields directly + // eslint-disable-next-line no-restricted-syntax + fields: Object.fromEntries(fieldsIdentifiers) as FieldIdentifiers, + }; +}; diff --git a/packages/shared/src/database/sql.test.ts b/packages/core/src/utils/sql.test.ts similarity index 98% rename from packages/shared/src/database/sql.test.ts rename to packages/core/src/utils/sql.test.ts index 6fb0b88f2..fa5601068 100644 --- a/packages/shared/src/database/sql.test.ts +++ b/packages/core/src/utils/sql.test.ts @@ -1,3 +1,4 @@ +import type { Table } from '@logto/shared'; import { sql } from 'slonik'; import { SqlToken } from 'slonik/dist/src/tokens.js'; @@ -9,7 +10,6 @@ import { convertToTimestamp, conditionalSql, } from './sql.js'; -import type { Table } from './types.js'; const { jest } = import.meta; diff --git a/packages/shared/src/database/sql.ts b/packages/core/src/utils/sql.ts similarity index 80% rename from packages/shared/src/database/sql.ts rename to packages/core/src/utils/sql.ts index 028bd4980..038327f2a 100644 --- a/packages/shared/src/database/sql.ts +++ b/packages/core/src/utils/sql.ts @@ -1,20 +1,14 @@ +import type { SchemaValue, SchemaValuePrimitive, Table } from '@logto/shared'; import type { Falsy } from '@silverhand/essentials'; import { notFalsy } from '@silverhand/essentials'; -import type { SqlSqlToken, SqlToken, QueryResult, IdentifierSqlToken } from 'slonik'; +import type { SqlSqlToken, SqlToken, IdentifierSqlToken } from 'slonik'; import { sql } from 'slonik'; -import type { FieldIdentifiers, SchemaValue, SchemaValuePrimitive, Table } from './types.js'; - export const conditionalSql = (value: T, buildSql: (value: Exclude) => SqlSqlToken) => notFalsy(value) ? buildSql(value) : sql``; -export const conditionalArraySql = ( - value: T[], - buildSql: (value: Exclude) => SqlSqlToken -) => (value.length > 0 ? buildSql(value) : sql``); export const autoSetFields = Object.freeze(['tenantId', 'createdAt', 'updatedAt'] as const); -export type OmitAutoSetFields = Omit; -export type ExcludeAutoSetFields = Exclude; + export const excludeAutoSetFields = (fields: readonly T[]) => Object.freeze( fields.filter( @@ -33,10 +27,10 @@ export const excludeAutoSetFields = (fields: readonly T[]) => * @param value The value to convert. * @returns A primitive that can be saved into database. */ - export const convertToPrimitiveOrSql = ( key: string, value: SchemaValue + // eslint-disable-next-line @typescript-eslint/ban-types ): NonNullable | SqlToken | null => { if (value === null) { return null; @@ -68,6 +62,10 @@ export const convertToPrimitiveOrSql = ( throw new Error(`Cannot convert ${key} to primitive`); }; +type FieldIdentifiers = { + [key in Key]: IdentifierSqlToken; +}; + export const convertToIdentifiers = ( { table, fields }: Table, withPrefix = false @@ -87,9 +85,3 @@ export const convertToIdentifiers = ( export const convertToTimestamp = (time = new Date()) => sql`to_timestamp(${time.valueOf() / 1000})`; - -export const manyRows = async (query: Promise>): Promise => { - const { rows } = await query; - - return rows; -}; diff --git a/packages/shared/package.json b/packages/shared/package.json index cf2485e74..c041a20d7 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -64,7 +64,6 @@ "chalk": "^5.0.0", "find-up": "^7.0.0", "libphonenumber-js": "^1.9.49", - "nanoid": "^5.0.1", - "slonik": "^30.0.0" + "nanoid": "^5.0.1" } } diff --git a/packages/shared/src/database/types.ts b/packages/shared/src/database/types.ts index 0508100cf..fde43ae93 100644 --- a/packages/shared/src/database/types.ts +++ b/packages/shared/src/database/types.ts @@ -1,5 +1,3 @@ -import type { IdentifierSqlToken } from 'slonik'; - export type SchemaValuePrimitive = string | number | boolean | undefined; export type SchemaValue = SchemaValuePrimitive | Record | unknown[] | null; export type SchemaLike = { @@ -10,9 +8,6 @@ export type Table = { table: TableName; fields: Record; }; -export type FieldIdentifiers = { - [key in Key]: IdentifierSqlToken; -}; export type OrderDirection = 'asc' | 'desc'; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index bd0487a64..9ae771b27 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,2 @@ export * from './universal.js'; export * from './node/index.js'; -export * from './database/sql.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f260d4d71..066748a86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4078,9 +4078,6 @@ importers: nanoid: specifier: ^5.0.1 version: 5.0.1 - slonik: - specifier: ^30.0.0 - version: 30.1.2 devDependencies: '@logto/connector-kit': specifier: workspace:^2.1.0