From f7a4d4465c5bc92dba2cdd1531afb5fae01f19a9 Mon Sep 17 00:00:00 2001 From: Saenyakorn Siangsanoh Date: Fri, 16 May 2025 18:06:16 +0700 Subject: [PATCH 1/3] feat: rewrite server function --- examples/erp/drizzlify/config.ts | 4 +- packages/core/src/auth/context.ts | 4 +- .../core/src/auth/handlers/forgot-password.ts | 21 +- packages/core/src/auth/handlers/me.ts | 22 +- .../core/src/auth/handlers/reset-password.ts | 28 +-- .../core/src/auth/handlers/sign-in-email.ts | 25 +- packages/core/src/auth/handlers/sign-out.ts | 12 +- packages/core/src/auth/handlers/sign-up.ts | 18 +- packages/core/src/auth/index.ts | 4 +- packages/core/src/auth/utils.ts | 18 +- packages/core/src/config.ts | 17 +- packages/core/src/endpoint.ts | 93 +++---- packages/core/src/index.ts | 3 +- packages/core/src/spec.ts | 6 +- packages/next/package.json | 2 +- packages/next/src/server-function.ts | 234 +++--------------- pnpm-lock.yaml | 2 +- 17 files changed, 158 insertions(+), 355 deletions(-) diff --git a/examples/erp/drizzlify/config.ts b/examples/erp/drizzlify/config.ts index de66ef90..c712c56f 100644 --- a/examples/erp/drizzlify/config.ts +++ b/examples/erp/drizzlify/config.ts @@ -23,7 +23,7 @@ export const serverConfig = baseConfig.toServerConfig({ }, ({ query }) => { return { - status: 200, + status: 200 as const, body: { message: `Hello ${query.name ?? 'World'}`, }, @@ -45,7 +45,7 @@ export const serverConfig = baseConfig.toServerConfig({ }, ({ query }) => { return { - status: 200, + status: 200 as const, body: { message: `Hello2 ${query.name ?? 'World'}`, }, diff --git a/packages/core/src/auth/context.ts b/packages/core/src/auth/context.ts index 81c76310..fc4e787a 100644 --- a/packages/core/src/auth/context.ts +++ b/packages/core/src/auth/context.ts @@ -26,7 +26,7 @@ export type AuthContext = { authConfig: TConfig internalHandlers: InternalHandlers - requiredAuthenticated: (headers: Headers) => Promise> + requiredAuthenticated: (headers: Record) => Promise> } export function createAuthContext( @@ -39,7 +39,7 @@ export function createAuthContext { + requiredAuthenticated: async (headers: Record) => { const sessionId = getSessionCookie(headers) if (!sessionId) throw new Error('Unauthorized') const session = await internalHandlers.session.findUserBySessionId(sessionId) diff --git a/packages/core/src/auth/handlers/forgot-password.ts b/packages/core/src/auth/handlers/forgot-password.ts index e4f91c99..9a767fe3 100644 --- a/packages/core/src/auth/handlers/forgot-password.ts +++ b/packages/core/src/auth/handlers/forgot-password.ts @@ -1,7 +1,6 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AuthContext } from '../context' import { WithPrefix } from '../types' @@ -14,18 +13,17 @@ export function forgotPasswordEmail ) { const schema = { method: 'POST', - path: (options.prefix ? `${options.prefix}/forgot-password` : '/forgot-password') as WithPrefix< - TOptions['prefix'], - '/forgot-password' - >, - body: z.interface({ + path: (options.prefix + ? `${options.prefix}/auth/forgot-password` + : '/auth/forgot-password') as WithPrefix, + body: z.object({ email: z.string(), }), responses: { - 200: z.interface({ + 200: z.object({ status: z.string(), }), - 400: z.interface({ + 400: z.object({ status: z.string(), }), }, @@ -63,8 +61,5 @@ export function forgotPasswordEmail } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/handlers/me.ts b/packages/core/src/auth/handlers/me.ts index 955125f6..049b7d7b 100644 --- a/packages/core/src/auth/handlers/me.ts +++ b/packages/core/src/auth/handlers/me.ts @@ -1,8 +1,8 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AuthContext } from '../context' +import { WithPrefix } from '../types' interface InternalRouteOptions { prefix?: string @@ -11,17 +11,18 @@ interface InternalRouteOptions { export function me(options: TOptions) { const schema = { method: 'GET', - path: (options.prefix ? `${options.prefix}/me` : '/me') as TOptions['prefix'] extends string - ? `${TOptions['prefix']}/me` - : '/me', + path: (options.prefix ? `${options.prefix}/auth/me` : '/auth/me') as WithPrefix< + TOptions['prefix'], + '/auth/me' + >, responses: { - 200: z.interface({ + 200: z.object({ id: z.string(), name: z.string(), email: z.string(), - 'image?': z.string().nullable(), + image: z.string().nullable().optional(), }), - 401: z.interface({ + 401: z.object({ status: z.string(), }), }, @@ -36,8 +37,5 @@ export function me(options: TOption } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/handlers/reset-password.ts b/packages/core/src/auth/handlers/reset-password.ts index c511f70c..d3379822 100644 --- a/packages/core/src/auth/handlers/reset-password.ts +++ b/packages/core/src/auth/handlers/reset-password.ts @@ -1,8 +1,8 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AuthContext } from '../context' +import { WithPrefix } from '../types' interface InternalRouteOptions { prefix?: string @@ -12,21 +12,19 @@ export function resetPasswordEmail( const schema = { method: 'POST', path: (options.prefix - ? `${options.prefix}/reset-password` - : '/reset-password') as TOptions['prefix'] extends string - ? `${TOptions['prefix']}/reset-password` - : '/reset-password', - query: z.interface({ + ? `${options.prefix}/auth/reset-password` + : '/auth/reset-password') as WithPrefix, + query: z.object({ token: z.string(), }), - body: z.interface({ + body: z.object({ password: z.string(), }), responses: { - 200: z.interface({ + 200: z.object({ status: z.string(), }), - 400: z.interface({ + 400: z.object({ status: z.string(), }), }, @@ -59,8 +57,9 @@ export function resetPasswordEmail( const redirectTo = `${args.context.authConfig.resetPassword?.redirectTo ?? '/auth/login'}` - const responseHeaders = new Headers() - responseHeaders.set('Location', redirectTo) + const responseHeaders = { + Location: redirectTo, + } return { status: 200, @@ -69,8 +68,5 @@ export function resetPasswordEmail( } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/handlers/sign-in-email.ts b/packages/core/src/auth/handlers/sign-in-email.ts index 08d78c21..49b657c7 100644 --- a/packages/core/src/auth/handlers/sign-in-email.ts +++ b/packages/core/src/auth/handlers/sign-in-email.ts @@ -1,7 +1,6 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AccountProvider } from '../constant' import { AuthContext } from '../context' import { WithPrefix } from '../types' @@ -14,23 +13,22 @@ interface InternalRouteOptions { export function signInEmail(options: TOptions) { const schema = { method: 'POST', - path: (options.prefix ? `${options.prefix}/sign-in/email` : '/sign-in/email') as WithPrefix< - TOptions['prefix'], - '/sign-in/email' - >, + path: (options.prefix + ? `${options.prefix}/auth/sign-in/email` + : '/auth/sign-in/email') as WithPrefix, - body: z.interface({ + body: z.object({ email: z.string(), password: z.string(), }), responses: { - 200: z.interface({ + 200: z.object({ token: z.string().nullable(), - user: z.interface({ + user: z.object({ id: z.string(), name: z.string(), email: z.string(), - 'image?': z.string().nullable(), + image: z.string().nullable().optional(), }), }), }, @@ -54,7 +52,7 @@ export function signInEmail(options expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), }) - const responseHeaders = new Headers() + const responseHeaders = {} setSessionCookie(responseHeaders, session.token) return { @@ -67,8 +65,5 @@ export function signInEmail(options } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/handlers/sign-out.ts b/packages/core/src/auth/handlers/sign-out.ts index 2f6bef03..b37e8b35 100644 --- a/packages/core/src/auth/handlers/sign-out.ts +++ b/packages/core/src/auth/handlers/sign-out.ts @@ -1,7 +1,6 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AuthContext } from '../context' import { WithPrefix } from '../types' import { deleteSessionCookie, getSessionCookie } from '../utils' @@ -13,13 +12,13 @@ interface InternalRouteOptions { export function signOut(options: TOptions) { const schema = { method: 'POST', - path: (options.prefix ? `${options.prefix}/sign-out` : '/sign-out') as WithPrefix< + path: (options.prefix ? `${options.prefix}/auth/sign-out` : '/auth/sign-out') as WithPrefix< TOptions['prefix'], '/sign-out' >, body: undefined, responses: { - 200: z.interface({ + 200: z.object({ status: z.string(), }), }, @@ -45,8 +44,5 @@ export function signOut(options: TO } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/handlers/sign-up.ts b/packages/core/src/auth/handlers/sign-up.ts index a7eb6088..64377fd9 100644 --- a/packages/core/src/auth/handlers/sign-up.ts +++ b/packages/core/src/auth/handlers/sign-up.ts @@ -1,7 +1,6 @@ import z from 'zod' -import { ApiRoute, ApiRouteHandler, ApiRouteSchema } from '~/core/endpoint' - +import { ApiRouteHandler, ApiRouteSchema, createEndpoint } from '../../endpoint' import { AccountProvider } from '../constant' import { AuthContext } from '../context' import { WithPrefix } from '../types' @@ -13,12 +12,12 @@ interface InternalRouteOptions { export function signUp(options: TOptions) { const schema = { method: 'POST', - path: (options.prefix ? `${options.prefix}/sign-up` : '/sign-up') as WithPrefix< + path: (options.prefix ? `${options.prefix}/auth/sign-up` : '/auth/sign-up') as WithPrefix< TOptions['prefix'], - '/sign-up' + '/auth/sign-up' >, body: z - .interface({ + .object({ name: z.string(), email: z.string(), password: z.string(), @@ -26,9 +25,9 @@ export function signUp(options: TOp }) .and(z.record(z.string(), z.any())), responses: { - 200: z.interface({ + 200: z.object({ token: z.string().nullable(), - user: z.interface({ + user: z.object({ id: z.string(), name: z.string(), email: z.string(), @@ -73,8 +72,5 @@ export function signUp(options: TOp } } - return { - ...schema, - handler, - } satisfies ApiRoute + return createEndpoint(schema, handler) } diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 0175f8ea..ac81277f 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -103,7 +103,7 @@ type ChangeAuthHandlerContextToMinimalContext< any, infer TApiRouteSchema > - ? TApiRouteSchema & { handler: ApiRouteHandler } + ? { schema: TApiRouteSchema; handler: ApiRouteHandler } : never } @@ -140,7 +140,7 @@ export function createAuth( const handler: ApiRouteHandler = (args) => { return h.handler({ ...args, context: authContext } as any) as any } - return { ...h, handler } + return { schema: h.schema, handler } }) as ChangeAuthHandlerContextToMinimalContext< TContext, ReturnType['handlers'] diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 847974fb..a226f785 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -1,9 +1,9 @@ -function setCookie(headers: Headers, name: string, value: string) { - headers.append('Set-Cookie', `${name}=${value}; Path=/; HttpOnly; SameSite=Strict`) +function setCookie(headers: Record, name: string, value: string) { + headers['Set-Cookie'] = `${name}=${value}; Path=/; HttpOnly; SameSite=Strict` } -function getCookie(headers: Headers, name: string) { - const cookie = headers.get('Cookie') +function getCookie(headers: Record, name: string) { + const cookie = headers[name] if (!cookie) return null const cookies = cookie.split('; ') for (const c of cookies) { @@ -13,20 +13,20 @@ function getCookie(headers: Headers, name: string) { return null } -function deleteCookie(headers: Headers, name: string) { - headers.append('Set-Cookie', `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`) +function deleteCookie(headers: Record, name: string) { + headers['Set-Cookie'] = `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0` } const SESSION_COOKIE_NAME = 'SESSION_ID' -export function getSessionCookie(headers: Headers) { +export function getSessionCookie(headers: Record) { return getCookie(headers, SESSION_COOKIE_NAME) } -export function setSessionCookie(headers: Headers, value: string) { +export function setSessionCookie(headers: Record, value: string) { setCookie(headers, SESSION_COOKIE_NAME, value) } -export function deleteSessionCookie(headers: Headers) { +export function deleteSessionCookie(headers: Record) { deleteCookie(headers, SESSION_COOKIE_NAME) } diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 65076aa9..8e2fab94 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -50,7 +50,7 @@ export interface ServerConfig< > extends BaseConfig { context: TContext collections: TCollections - endpoints?: TApiRouter + endpoints: TApiRouter } export type InferApiRouterFromServerConfig> = @@ -83,11 +83,6 @@ export function defineBaseConfig< const auth = createAuth(config.auth, context) const collectionEndpoints = getAllCollectionEndpoints(args.collections) - type Endpoints = TEndpoints & - typeof auth.handlers & - ExtractAllCollectionCustomEndpoints & - ExtractAllCollectionDefaultEndpoints - return { ...config, context: context, @@ -96,12 +91,18 @@ export function defineBaseConfig< ...args.endpoints, ...auth.handlers, ...collectionEndpoints, - } as Endpoints, + } as TEndpoints & + typeof auth.handlers & + ExtractAllCollectionCustomEndpoints & + ExtractAllCollectionDefaultEndpoints, } satisfies ServerConfig< TFullSchema, MinimalContext, TCollections, - Endpoints + TEndpoints & + typeof auth.handlers & + ExtractAllCollectionCustomEndpoints & + ExtractAllCollectionDefaultEndpoints > } diff --git a/packages/core/src/endpoint.ts b/packages/core/src/endpoint.ts index 8321af48..8ffea4c9 100644 --- a/packages/core/src/endpoint.ts +++ b/packages/core/src/endpoint.ts @@ -1,56 +1,70 @@ import { ExtractObjectValues, Simplify } from 'drizzle-orm' -import type { ConditionalExcept } from 'type-fest' +import { IsNever, SimplifyDeep } from 'type-fest' import { z, ZodType } from 'zod' import { MaybePromise } from './collection' export type ApiHttpStatus = 200 | 201 | 204 | 301 | 302 | 400 | 401 | 403 | 404 | 409 | 422 | 500 -export type InputSchema = ZodType | undefined +export type InputSchema = ZodType | undefined export type Output = - T extends ZodType ? z.output : never + T extends ZodType ? z.output : never export type InferPathParams = Simplify< TPath extends `${string}/:${infer TRest}` ? { [key in TRest as TRest extends `${infer THead}/${string}` ? THead : TRest]: string - } & InferPathParams + } & (IsNever> extends true ? {} : InferPathParams) + : never +> + +// TODO: With IsNever, the performance is not good. Need to fix it. +type GetBody = + TApiRouteSchema extends ApiRouteMutationSchema + ? IsNever extends false + ? { body: Output } + : {} : {} + +type GetHeaders = { + headers: IsNever> extends false + ? Output & Record + : Record +} + +type GetQuery = + IsNever> extends false + ? { query: Output } + : {} + +type GetPathParams = + IsNever> extends false + ? { pathParams: Output } + : IsNever> extends false + ? { pathParams: InferPathParams } + : {} + +export type ApiRouteHandlerPayload = SimplifyDeep< + GetBody & + GetHeaders & + GetQuery & + GetPathParams > -export type ApiRouteHandlerPayload< +export type ApiRouteHandlerPayloadWithContext< + TApiRouteSchema extends ApiRouteSchema, TContext extends Record = Record, - TApiRouteSchema extends ApiRouteSchema = ApiRouteSchema, -> = { - body: TApiRouteSchema extends ApiRouteMutationSchema ? Output : never - headers: Output - query: Output - pathParams: TApiRouteSchema['pathParams'] extends InputSchema - ? InferPathParams & Output - : InferPathParams +> = ApiRouteHandlerPayload & { context: TContext } -export type ClientApiRouteHandlerPayload = - ConditionalExcept< - { - body: TApiRouteSchema extends ApiRouteMutationSchema ? Output : never - headers: Output - query: Output - pathParams: TApiRouteSchema['pathParams'] extends InputSchema - ? InferPathParams & Output - : InferPathParams - }, - never - > - export type ApiRouteResponse>> = ExtractObjectValues<{ [TStatus in keyof TResponses]: TResponses[TStatus] extends InputSchema ? { status: TStatus body: Output - headers?: Headers + headers?: Record } : never }> @@ -59,7 +73,7 @@ export type ApiRouteHandler< TContext extends Record = Record, TApiRouteSchema extends ApiRouteSchema = ApiRouteSchema, > = ( - payload: ApiRouteHandlerPayload + payload: ApiRouteHandlerPayloadWithContext ) => MaybePromise> export type GetApiRouteSchemaFromApiRouteHandler< @@ -69,18 +83,6 @@ export type GetApiRouteSchemaFromApiRouteHandler< ? TApiRouteSchema : never -export type ApiRouteMiddleware< - TContext extends Record = Record, - TApiRouteSchema extends ApiRouteSchema = ApiRouteSchema, -> = (args: { - payload: ApiRouteHandlerPayload - req: Request - next: (args: { - payload: ApiRouteHandlerPayload - req: Request - }) => MaybePromise -}) => MaybePromise - export interface ApiRouteCommonSchema { path: string pathParams?: InputSchema @@ -110,11 +112,10 @@ export type ApiRouteSchema = ApiRouteQuerySchema | ApiRouteMutationSchema export type ApiRoute< TContext extends Record = Record, TApiRouteSchema extends ApiRouteSchema = any, -> = Simplify< - TApiRouteSchema & { - handler: ApiRouteHandler - } -> +> = { + schema: TApiRouteSchema + handler: ApiRouteHandler +} export interface ApiRouter = Record> { [key: string]: ApiRoute @@ -136,7 +137,7 @@ export function createEndpoint< TContext extends Record = Record, >(schema: TApiEndpointSchema, handler: ApiRouteHandler) { return { - ...schema, + schema, handler, } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2b715028..8d5cf80a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,10 +20,11 @@ export type { BaseConfig, ClientConfig, ServerConfig } from './config' export { defineBaseConfig, getClientCollection, getClientConfig } from './config' export type { ApiRoute, + ApiRouteHandler, ApiRouteHandlerPayload, ApiRouter, + ApiRouteResponse, ApiRouteSchema, - ClientApiRouteHandlerPayload, InferApiRouteResponses, } from './endpoint' export { type Field, FieldBuilder } from './field' diff --git a/packages/core/src/spec.ts b/packages/core/src/spec.ts index 4a05627b..624cca2c 100644 --- a/packages/core/src/spec.ts +++ b/packages/core/src/spec.ts @@ -148,7 +148,7 @@ export const postCollection = builder.collection('posts', { ({ context, body }) => { const name = body.name return { - status: 200, + status: 200 as const, body: { hello: 's', }, @@ -171,7 +171,7 @@ export const postCollection = builder.collection('posts', { ({ context, body }) => { const name = body.name return { - status: 200, + status: 200 as const, body: { hello: 's', }, @@ -201,7 +201,7 @@ export const serverConfig = baseConfig.toServerConfig({ ({ context, body }) => { const name = body.name return { - status: 200, + status: 200 as const, body: { hello: name, }, diff --git a/packages/next/package.json b/packages/next/package.json index d9057444..d64c8897 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -45,7 +45,7 @@ "tailwind-merge": "^3.0.2", "tailwind-variants": "^1.0.0", "tw-animate-css": "^1.2.4", - "type-fest": "^4.38.0", + "type-fest": "^4.41.0", "vitest": "^3.0.9" } } diff --git a/packages/next/src/server-function.ts b/packages/next/src/server-function.ts index 20b68b9d..c47eff4d 100644 --- a/packages/next/src/server-function.ts +++ b/packages/next/src/server-function.ts @@ -1,225 +1,49 @@ -import { ExtractObjectValues, Simplify } from 'drizzle-orm' +import type { Simplify } from 'type-fest' import { - ApiDefaultMethod, - ApiReturnType, ApiRoute, ApiRouteHandlerPayload, ApiRouter, + ApiRouteResponse, ApiRouteSchema, - ClientApiArgs, - ClientApiRouteHandlerPayload, - Collection, - InferApiRouteResponses, - InferApiRouterFromCollection, ServerConfig, } from '@kivotos/core' +type ExtractObjectValues = T[keyof T] + export type ServerFunction> = < - const TMethod extends GetServerMethod = GetServerMethod, + TApiArgs extends GetServerFunctionApiArgs, >( - args: TMethod -) => Promise> - -export type DefaultMethodFromCollection< - TCollection extends Collection, -> = - TCollection extends Collection - ? Simplify< - ExtractObjectValues<{ - [TMethod in ApiDefaultMethod]: TMethod extends ApiDefaultMethod - ? { - slug: TCollection['slug'] - method: TMethod - payload: Simplify> - } - : never - }> - > - : never - -export type CustomMethodFromCollection< - TCollection extends Collection, -> = - TCollection extends Collection - ? Simplify< - ExtractObjectValues<{ - [TMethod in keyof InferApiRouterFromCollection]: { - slug: TCollection['slug'] - method: TMethod - payload: InferApiRouterFromCollection[TMethod] extends ApiRouteSchema - ? Simplify< - ClientApiRouteHandlerPayload[TMethod]> - > - : never - } - }> - > - : never - -type AllMethodFromCollection> = - TCollection extends Collection - ? Simplify | CustomMethodFromCollection> - : never - -export type CustomMethodFromServerConfig = - Exclude extends infer TRouter extends ApiRouter - ? ExtractObjectValues<{ - [TMethod in keyof TRouter]: { - slug: 'custom' - method: TMethod - payload: TRouter[TMethod] extends ApiRouteSchema - ? Simplify> - : never - } - }> - : never - -type CustomMethodFromServerConfigResponse< - TServerConfig extends ServerConfig, - TMethodName extends string, -> = - Exclude extends infer TRouter extends ApiRouter - ? TMethodName extends keyof TRouter - ? TRouter[TMethodName] extends infer TRoute extends ApiRouteSchema - ? Simplify> - : never - : never - : never - -type CustomMethodFromCollectionResponse< - TCollection extends Collection, - TMethodName extends string, -> = Exclude[TMethodName] extends infer TRoute extends - ApiRouteSchema - ? TRoute extends ApiRouteSchema - ? Simplify> - : never - : never + args: TApiArgs +) => Promise> -type DefaultMethodFromCollectionResponse< - TCollection extends Collection, - TMethodName extends string, -> = TMethodName extends ApiDefaultMethod - ? Simplify> +type GetServerFunctionResponse< + TServerConfig extends ServerConfig, + TMethod extends keyof TServerConfig['endpoints'], +> = TServerConfig['endpoints'][TMethod] extends infer TApiRoute extends ApiRoute + ? ApiRouteResponse : never -export type GetServerMethodResponse< - TServerConfig extends ServerConfig, - TMethod extends GetServerMethod, -> = TMethod['slug'] extends 'custom' - ? CustomMethodFromServerConfigResponse - : TMethod['method'] extends ApiDefaultMethod - ? DefaultMethodFromCollectionResponse< - Extract, - TMethod['method'] - > - : CustomMethodFromCollectionResponse< - Extract, - TMethod['method'] - > - -export type GetServerMethod> = Simplify< - | AllMethodFromCollection - | CustomMethodFromServerConfig -> - -function isCustomMethodFromServerConfig(method: TMethod) { - return (method as any).slug === 'custom' -} - -function isCustomMethodFromCollection( - method: TMethod -) { - return ( - (method as any).slug !== 'custom' && - !Object.values(ApiDefaultMethod).includes(method.method as any) - ) -} - -function isDefaultMethodFromCollection( - method: TMethod -) { - return ( - (method as any).slug !== 'custom' && - Object.values(ApiDefaultMethod).includes(method.method as any) - ) -} +export type GetServerFunctionApiArgs | undefined> = + ExtractObjectValues<{ + [TMethod in Extract]: TApiRouter[TMethod] extends ApiRoute< + any, + infer TApiRouteSchema extends ApiRouteSchema + > + ? Simplify<{ method: TMethod } & ApiRouteHandlerPayload> + : never + }> export async function handleServerFunction< - TServerConfig extends ServerConfig, - TMethod extends GetServerMethod, + TServerConfig extends ServerConfig, + TApiArgs extends GetServerFunctionApiArgs, >( serverConfig: TServerConfig, - method: TMethod -): Promise> { - const slug = method.slug - - if (isCustomMethodFromServerConfig(method)) { - const handlers = serverConfig.endpoints - if (!handlers) throw new Error('No endpoints found in server config') - - const handler = handlers[method.method] as ApiRoute - if (!handler) { - throw new Error(`Method ${method.method} not found`) - } - - const result = await handler.handler({ - context: serverConfig.context, - ...(method.payload as any), - } as ApiRouteHandlerPayload) - - return result as GetServerMethodResponse - } else if (isCustomMethodFromCollection(method)) { - const collection = serverConfig.collections.find( - (collection: Collection) => collection.slug === slug - ) - if (!collection) { - throw new Error(`Collection ${slug} not found`) - } - - const handlers = collection.admin.endpoints - - const handler = handlers?.[method.method] as ApiRoute - if (!handler) { - throw new Error(`Method ${method.method} not found`) - } - - const result = await handler.handler({ - context: { ...serverConfig.context, db: serverConfig.db }, - ...(method.payload as any), - } as ApiRouteHandlerPayload) - - return result as GetServerMethodResponse - } else if (isDefaultMethodFromCollection(method)) { - const collection = serverConfig.collections.find( - (collection: Collection) => collection.slug === slug - ) - if (!collection) { - throw new Error(`Collection ${slug} not found`) - } - - const handlers = collection.admin.api - - const handler = handlers[method.method as ApiDefaultMethod] - if (!handler) { - throw new Error(`Method ${method.method} not found`) - } - - // TODO: Fix request - const request = new Request('') - const result = await handler( - { - context: { ...serverConfig.context, db: serverConfig.db }, - fields: collection.fields, - slug: collection.slug, - ...(method.payload as any), - } as any, - request - ) - - return result as GetServerMethodResponse + args: TApiArgs +): Promise> { + const apiRoute = serverConfig.endpoints?.[args.method as keyof typeof serverConfig.endpoints] + if (!apiRoute) { + throw new Error(`No API route found for method: ${args.method}`) } - - throw new Error(`Method ${method.method} not found`) + return apiRoute.handler({ ...args, context: serverConfig.context }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 131fb487..6449c366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,7 +270,7 @@ importers: specifier: ^1.2.4 version: 1.2.9 type-fest: - specifier: ^4.38.0 + specifier: ^4.41.0 version: 4.41.0 vitest: specifier: ^3.0.9 From 8c4bc369cafa6fd7c518de4180e213f7b205cd82 Mon Sep 17 00:00:00 2001 From: Saenyakorn Siangsanoh <33742791+saenyakorn@users.noreply.github.com> Date: Fri, 16 May 2025 18:16:07 +0700 Subject: [PATCH 2/3] Create itchy-peaches-peel.md --- .changeset/itchy-peaches-peel.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/itchy-peaches-peel.md diff --git a/.changeset/itchy-peaches-peel.md b/.changeset/itchy-peaches-peel.md new file mode 100644 index 00000000..9592623e --- /dev/null +++ b/.changeset/itchy-peaches-peel.md @@ -0,0 +1,7 @@ +--- +"@example/erp": patch +"@kivotos/core": minor +"@kivotos/next": minor +--- + +[[BRV-89] Server Function RPC](https://app.plane.so/softnetics/browse/BRV-89/) From 82a92ad7962ea2ea6e436fb78a3982ab460575fd Mon Sep 17 00:00:00 2001 From: Saenyakorn Siangsanoh <33742791+saenyakorn@users.noreply.github.com> Date: Mon, 19 May 2025 17:05:18 +0700 Subject: [PATCH 3/3] Update .changeset/itchy-peaches-peel.md --- .changeset/itchy-peaches-peel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/itchy-peaches-peel.md b/.changeset/itchy-peaches-peel.md index 9592623e..fb53b74c 100644 --- a/.changeset/itchy-peaches-peel.md +++ b/.changeset/itchy-peaches-peel.md @@ -4,4 +4,4 @@ "@kivotos/next": minor --- -[[BRV-89] Server Function RPC](https://app.plane.so/softnetics/browse/BRV-89/) +[[DRIZZ-39] Server Function RPC](https://app.plane.so/softnetics/browse/DRIZZ-39/)