samchon / nestia

NestJS Helper Libraries + TypeScript OpenAPI generator
https://nestia.io/
MIT License
1.76k stars 91 forks source link

Global connection #821

Closed Darkbound closed 5 months ago

Darkbound commented 5 months ago

Question

Hello, this seems like a great tool, I've been looking for something to use to generate SDK from my Nestjs project for the last week and found this, its great!

However, I still cant figure out exactly how to use the connection thing, isnt there a way to globally define the host for the whole API, instead of having to add it to every single function?

samchon commented 5 months ago

You can accomplish it by utilizing the type operation.

import typia from "typia";

import api from "@api";
import { IBbsArticle } from "@api/lib/structures/IBbsArticle";

export const test_api_bbs_article_store = async (
  connection: api.IConnection,
): Promise<void> => {
  const article: IBbsArticle = await api.functional.bbs.articles.store(
    connection,
    typia.random<IBbsArticle.IStore>(),
  );
  typia.assertEquals(article);

  const binded: IBbsArticle = await sdkFunctionBinder(
    api.functional.bbs.articles.store,
  )(typia.random<IBbsArticle.IStore>());
  typia.assertEquals(binded);
};

const sdkFunctionBinder = <
  SdkFunction extends (
    connection: api.IConnection,
    ...args: any[]
  ) => Promise<any>,
>(
  func: SdkFunction,
): SdkFunction extends (
  connection: api.IConnection,
  ...args: infer Args
) => Promise<infer Result>
  ? (...args: Args) => Promise<Result>
  : never => ((...args: any[]) => func(globalConnection, ...args)) as any;
const globalConnection: api.IConnection = {
  host: "http://localhost:3000",
};
Darkbound commented 5 months ago

Thank you for the quick response!

This would work, I thought of something similar, but this still requires wrapping all of the calls, its too much hassle for something like this, I think that the client should expose a function or object where we can add global configuration just once, and then the connection object should become optional for each of the functions, in case you want to do something different from the global.

samchon commented 5 months ago

What about using the sdkFunctionBinder function manually for every API calls?

The global variable configuration is too domestic, so that cannot support in the library level.

Darkbound commented 5 months ago

I don't want to have to do anything for every API call, I want to have to define it just once and move on with my life as they say :D

I will try to come up with a wrapper that does this and post back if I succeed, it would be very useful to have it

Darkbound commented 5 months ago

@samchon I have created the wrapper, how do you want to do this if you want to check it out

samchon commented 5 months ago

If succeeded to composing your wrapper, you just need to close this issue.

Darkbound commented 5 months ago

@samchon yes, I have succeeded, my question is if you want to check it out and provide it to the community

samchon commented 5 months ago

If me, I'll put the global connection utility into the SDK, so that publish at the same time.

  1. Let's assume that your utility file is src/api/utils/MyGlobalConnection.ts
  2. Export it in the src/api/module.ts
  3. Publish the SDK, then client developers can use it directly
yadav-saurabh commented 3 months ago

@samchon yes, I have succeeded, my question is if you want to check it out and provide it to the community

hey @Darkbound, It will be really helpful if you can share the code.

zeallat commented 3 months ago

Leave the code for @yadav-saurabh, and for the next person who finds this thread like I did.

// Usage
export const ApiService = bindConnectionInSdk(api.functional, async () => ({
  host: 'http://localhost:3001',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    Authorization: `Bearer ${await AuthService.getCurrentToken()}`,
  },
  logger: async (event: any) => {
    // eslint-disable-next-line no-console
    console.log('api called', event);
  },
}));

// Implementation
type UnresolvedConnection = (() => Promise<api.IConnection>) | api.IConnection;

type SdkFunction = (
  connection: api.IConnection,
  ...args: any[]
) => Promise<any>;

type BoundSdkFunction<T extends SdkFunction> = T extends (
  connection: api.IConnection,
  ...args: infer Args
) => Promise<infer Result>
  ? (...args: Args) => Promise<Result>
  : never;

type Sdk = {
  [key: string]: Sdk | ((...args: any[]) => any);
};

type BoundSdk<T extends Sdk> = {
  [K in keyof T]: T[K] extends SdkFunction
    ? BoundSdkFunction<T[K]>
    : T[K] extends Sdk
      ? BoundSdk<T[K]>
      : never;
};

type BoundSdkOrFunction<T> = T extends SdkFunction
  ? BoundSdkFunction<T>
  : T extends Sdk
    ? BoundSdk<T>
    : never;

const isGeneralSdkFunction = (value: any): value is SdkFunction =>
  typeof value === 'function' && value.length > 0;

const resolveConnection = async (
  unresolvedConnection: UnresolvedConnection,
) => {
  return typeof unresolvedConnection === 'function'
    ? await unresolvedConnection()
    : unresolvedConnection;
};

const bindConnectionInFunction = <T extends SdkFunction>(
  func: T,
  unresolvedConnection: UnresolvedConnection,
): T extends SdkFunction ? BoundSdkFunction<T> : never =>
  (async (...args: any[]) => {
    const resolvedConnection = await resolveConnection(unresolvedConnection);
    return func(resolvedConnection, ...args);
  }) as any;

const bindConnectionInSdkOrFunction = <T extends Sdk | SdkFunction>(
  sdkOrFunction: T,
  unresolvedConnection: UnresolvedConnection,
): BoundSdkOrFunction<T> => {
  if (isGeneralSdkFunction(sdkOrFunction)) {
    return bindConnectionInFunction(sdkOrFunction, unresolvedConnection);
  } else if (typeof sdkOrFunction === 'object' && sdkOrFunction !== null) {
    const copy: any = Array.isArray(sdkOrFunction) ? [] : {};
    for (const key in sdkOrFunction) {
      if (Object.prototype.hasOwnProperty.call(sdkOrFunction, key)) {
        if (
          typeof sdkOrFunction[key] === 'function' ||
          typeof sdkOrFunction[key] === 'object'
        ) {
          copy[key] = bindConnectionInSdkOrFunction(
            sdkOrFunction[key] as any,
            unresolvedConnection,
          );
        } else {
          copy[key] = sdkOrFunction[key];
        }
      }
    }
    return copy;
  }
  return sdkOrFunction as never;
};

const bindConnectionInSdk = <T extends Sdk>(
  sdk: T,
  unresolvedConnection: UnresolvedConnection,
): BoundSdk<T> =>
  bindConnectionInSdkOrFunction(sdk, unresolvedConnection) as BoundSdk<T>;