refactor: @formbricks/api package (#782)

* init: rewritten formbricks api package

* restrucure: client prepended formbricks api package

* feat: cleanup error templating and node-fetch for non browser access

* feat: replace package (build error for js packge due to api)

* fix: rename methods & move error type

* fix: imports of api via js

* remove node-fetch

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Shubham Palriwala
2023-10-01 15:20:29 +05:30
committed by GitHub
parent e3069f7bab
commit 769ceb1fc2
22 changed files with 172 additions and 246 deletions

View File

@@ -3,7 +3,6 @@
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
import { env } from "@/env.mjs";
import { formbricksEnabled, updateResponse } from "@/lib/formbricks";
import { ResponseId } from "@formbricks/js";
import { cn } from "@formbricks/lib/cn";
import { Objective } from "@formbricks/types/templates";
import { TProfile } from "@formbricks/types/v1/profile";
@@ -14,7 +13,7 @@ import { toast } from "react-hot-toast";
type ObjectiveProps = {
next: () => void;
skip: () => void;
formbricksResponseId?: ResponseId;
formbricksResponseId?: string;
profile: TProfile;
};

View File

@@ -10,7 +10,6 @@ import Greeting from "./Greeting";
import Objective from "./Objective";
import Product from "./Product";
import Role from "./Role";
import { ResponseId } from "@formbricks/js";
import { TProfile } from "@formbricks/types/v1/profile";
import { TProduct } from "@formbricks/types/v1/product";
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
@@ -25,7 +24,7 @@ interface OnboardingProps {
}
export default function Onboarding({ session, environmentId, profile, product }: OnboardingProps) {
const [formbricksResponseId, setFormbricksResponseId] = useState<ResponseId | undefined>();
const [formbricksResponseId, setFormbricksResponseId] = useState<string | undefined>();
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

View File

@@ -4,7 +4,6 @@ import { cn } from "@formbricks/lib/cn";
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
import { env } from "@/env.mjs";
import { createResponse, formbricksEnabled } from "@/lib/formbricks";
import { ResponseId, SurveyId } from "@formbricks/js";
import { TProfile } from "@formbricks/types/v1/profile";
import { Button } from "@formbricks/ui";
import { useState } from "react";
@@ -13,7 +12,7 @@ import { toast } from "react-hot-toast";
type RoleProps = {
next: () => void;
skip: () => void;
setFormbricksResponseId: (id: ResponseId) => void;
setFormbricksResponseId: (id: string) => void;
profile: TProfile;
};
@@ -49,7 +48,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
console.error(e);
}
if (formbricksEnabled && env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID) {
const res = await createResponse(env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID as SurveyId, {
const res = await createResponse(env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID, {
role: selectedRole.label,
});
if (res.ok) {

View File

@@ -1,17 +1,17 @@
import formbricks, { PersonId, SurveyId, ResponseId } from "@formbricks/js";
import formbricks from "@formbricks/js";
import { env } from "@/env.mjs";
export const formbricksEnabled =
typeof env.NEXT_PUBLIC_FORMBRICKS_API_HOST && env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID;
export const createResponse = async (
surveyId: SurveyId,
surveyId: string,
data: { [questionId: string]: any },
finished: boolean = false
): Promise<any> => {
const api = formbricks.getApi();
const personId = formbricks.getPerson()?.id as PersonId;
return await api.createResponse({
const personId = formbricks.getPerson()?.id;
return await api.client.response.create({
surveyId,
personId,
finished,
@@ -20,12 +20,12 @@ export const createResponse = async (
};
export const updateResponse = async (
responseId: ResponseId,
responseId: string,
data: { [questionId: string]: any },
finished: boolean = false
): Promise<any> => {
const api = formbricks.getApi();
return await api.updateResponse({
return await api.client.response.update({
responseId,
finished,
data,

View File

@@ -17,13 +17,12 @@
"lint": "eslint ./src --fix",
"clean": "rimraf .turbo node_modules dist"
},
"dependencies": {
"@formbricks/lib": "workspace:*"
},
"devDependencies": {
"@formbricks/types": "workspace:*",
"@formbricks/lib": "workspace:*",
"@formbricks/tsconfig": "workspace:*",
"eslint-config-formbricks": "workspace:*",
"tsup": "^7.2.0"
"tsup": "^7.2.0",
"typescript": "5.1.6",
"eslint-config-formbricks": "workspace:*"
}
}

View File

@@ -0,0 +1,23 @@
import { Result } from "@formbricks/types/v1/errorHandlers";
import { NetworkError } from "@formbricks/types/v1/errors";
import { makeRequest } from "../../utils/makeRequest";
import { TDisplay, TDisplayInput } from "@formbricks/types/v1/displays";
export class DisplayAPI {
private apiHost: string;
constructor(baseUrl: string) {
this.apiHost = baseUrl;
}
async markDisplayedForPerson({
surveyId,
personId,
}: TDisplayInput): Promise<Result<TDisplay, NetworkError | Error>> {
return makeRequest(this.apiHost, "/api/v1/client/displays", "POST", { surveyId, personId });
}
async markResponded({ displayId }: { displayId: string }): Promise<Result<TDisplay, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/displays/${displayId}/responded`, "POST");
}
}

View File

@@ -0,0 +1,15 @@
import { ResponseAPI } from "./response";
import { DisplayAPI } from "./display";
import { ApiConfig } from "../../types";
export class Client {
response: ResponseAPI;
display: DisplayAPI;
constructor(options: ApiConfig) {
const { apiHost } = options;
this.response = new ResponseAPI(apiHost);
this.display = new DisplayAPI(apiHost);
}
}

View File

@@ -0,0 +1,39 @@
import { makeRequest } from "../../utils/makeRequest";
import { NetworkError } from "@formbricks/types/v1/errors";
import { Result } from "@formbricks/types/v1/errorHandlers";
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/v1/responses";
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
export class ResponseAPI {
private apiHost: string;
constructor(apiHost: string) {
this.apiHost = apiHost;
}
async create({
surveyId,
personId,
finished,
data,
}: TResponseInput): Promise<Result<TResponse, NetworkError | Error>> {
return makeRequest(this.apiHost, "/api/v1/client/responses", "POST", {
surveyId,
personId,
finished,
data,
});
}
async update({
responseId,
finished,
data,
}: TResponseUpdateInputWithResponseId): Promise<Result<TResponse, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/responses/${responseId}`, "PUT", {
finished,
data,
});
}
}

View File

@@ -1 +0,0 @@
export * from "./responses";

View File

@@ -1,24 +0,0 @@
import { KeyValueData, PersonId, ResponseId, SurveyId } from "../types";
export interface CreateResponseResponse {
id: ResponseId;
}
export interface UpdateResponseResponse {
id: ResponseId;
createdAt: string;
updatedAt: string;
finished: boolean;
surveyId: SurveyId;
personId: PersonId;
data: KeyValueData;
meta: {}; //TODO: figure out what this is
userAttributes: string[]; //TODO: figure out what this is
tags: string[]; //TODO: figure out what this is
}
export interface UpdateResponseResponseFormatted
extends Omit<UpdateResponseResponse, "createdAt" | "updatedAt"> {
createdAt: Date;
updatedAt: Date;
}

View File

@@ -1,70 +0,0 @@
import { Result, ok } from "@formbricks/types/v1/errorHandlers";
import { ResponseCreateRequest, ResponseUpdateRequest } from "@formbricks/types/js";
import {
CreateResponseResponse,
UpdateResponseResponse,
UpdateResponseResponseFormatted,
} from "../dtos/responses";
import { NetworkError } from "../errors";
import { EnvironmentId, KeyValueData, PersonId, RequestFn, ResponseId, SurveyId } from "../types";
export interface CreateResponseOptions {
environmentId: EnvironmentId;
surveyId: SurveyId;
personId?: PersonId;
data: KeyValueData;
finished?: boolean;
}
export const createResponse = async (
request: RequestFn,
options: CreateResponseOptions
): Promise<Result<CreateResponseResponse, NetworkError>> => {
const result = await request<CreateResponseResponse, any, ResponseCreateRequest>(
`/api/v1/client/environments/${options.environmentId}/responses`,
{
surveyId: options.surveyId,
personId: options.personId,
response: {
data: options.data,
finished: options.finished || false,
},
},
{ method: "POST" }
);
return result;
};
export interface UpdateResponseOptions {
environmentId: EnvironmentId;
data: KeyValueData;
responseId: ResponseId;
finished?: boolean;
}
export const updateResponse = async (request: RequestFn, options: UpdateResponseOptions) => {
const result = await request<UpdateResponseResponse, any, ResponseUpdateRequest>(
`/api/v1/client/environments/${options.environmentId}/responses/${options.responseId}`,
{
response: {
data: options.data,
finished: options.finished || false,
},
},
{
method: "PUT",
}
);
if (result.ok === false) return result;
// convert timestamps to Dates
const newResponse: UpdateResponseResponseFormatted = {
...result.data,
createdAt: new Date(result.data.createdAt),
updatedAt: new Date(result.data.updatedAt),
};
return ok(newResponse);
};

View File

@@ -1,6 +0,0 @@
export type NetworkError = {
code: "network_error";
message: string;
status: number;
url: URL;
};

View File

@@ -1,6 +1,10 @@
export * from "./dtos/";
export * from "./errors";
export * from "./lib";
export { FormbricksAPI as default } from "./lib";
// do not export RequestFn or Brand, they are internal
export type { EnvironmentId, KeyValueData, PersonId, ResponseId, SurveyId } from "./types";
import { ApiConfig } from "./types/index";
import { Client } from "./api/client";
export class FormbricksAPI {
client: Client;
constructor(options: ApiConfig) {
this.client = new Client(options);
}
}

View File

@@ -1,85 +0,0 @@
import { Result, err, ok, wrapThrows } from "@formbricks/types/v1/errorHandlers";
import { CreateResponseResponse, UpdateResponseResponseFormatted } from "./dtos/responses";
import { NetworkError } from "./errors";
import {
CreateResponseOptions,
UpdateResponseOptions,
createResponse,
updateResponse,
} from "./endpoints/response";
import { EnvironmentId, RequestFn } from "./types";
export interface FormbricksAPIOptions {
apiHost?: string;
environmentId: EnvironmentId;
}
export class FormbricksAPI {
private readonly baseUrl: string;
private readonly environmentId: EnvironmentId;
constructor(options: FormbricksAPIOptions) {
this.baseUrl = options.apiHost || "https://app.formbricks.com";
this.environmentId = options.environmentId;
this.request = this.request.bind(this);
}
async createResponse(
options: Omit<CreateResponseOptions, "environmentId">
): Promise<Result<CreateResponseResponse, NetworkError>> {
return this.runWithEnvironmentId(createResponse, options);
}
async updateResponse(
options: Omit<UpdateResponseOptions, "environmentId">
): Promise<Result<UpdateResponseResponseFormatted, NetworkError>> {
return this.runWithEnvironmentId(updateResponse, options);
}
/*
This was added to reduce code duplication
It checks that the function passed has the environmentId in the Options type
and automatically adds it to the options
*/
private runWithEnvironmentId<T, E, Options extends { environmentId: EnvironmentId }>(
fn: (request: RequestFn, options: Options) => Promise<Result<T, E>>,
options: Omit<Options, "environmentId">
): Promise<Result<T, E>> {
const newOptions = { environmentId: this.environmentId, ...options } as Options;
return fn(this.request, newOptions);
}
private async request<T = any, E = any, Data = any>(
path: string,
data: Data,
options?: RequestInit
): Promise<Result<T, E | NetworkError | Error>> {
const url = new URL(path, this.baseUrl);
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const body = JSON.stringify(data);
const res = wrapThrows(fetch)(url, { headers, body, ...options });
if (res.ok === false) return err(res.error);
const response = await res.data;
const resJson = await response.json();
if (!response.ok) {
return err({
code: "network_error",
message: response.statusText,
status: response.status,
url,
});
}
return ok(resJson as T);
}
}

View File

@@ -1,27 +0,0 @@
import { Result } from "@formbricks/types/v1/errorHandlers";
import { NetworkError } from "./errors";
// by using Brand, we can check that you can't pass to an environmentId a surveyId
type Brand<T, B> = T & { __brand: B };
export type EnvironmentId = Brand<string, "EnvironmentId">;
export type SurveyId = Brand<string, "SurveyId">;
export type PersonId = Brand<string, "PersonId">;
export type ResponseId = Brand<string, "ResponseId">;
export type KeyValueData = { [key: string]: string | number | string[] | number[] | undefined };
export type RequestFn = <T = any, E = any, Data = any>(
path: string,
data: Data,
options?: RequestInit
) => Promise<Result<T, E | NetworkError | Error>>;
// https://github.com/formbricks/formbricks/blob/fbfc80dd4ed5d768f0c549e179fd1aa10edc400a/apps/web/lib/api/response.ts
export interface ApiErrorResponse {
code: string;
message: string;
details: {
[key: string]: string | string[] | number | number[] | boolean | boolean[];
};
}

View File

@@ -0,0 +1,8 @@
export interface ApiConfig {
environmentId: string;
apiHost: string;
}
export type ApiResponse<T> = {
data: T;
};

View File

@@ -0,0 +1,36 @@
import { Result, err, ok, wrapThrows } from "@formbricks/types/v1/errorHandlers";
import { NetworkError } from "@formbricks/types/v1/errors";
import { ApiResponse } from "../types";
export async function makeRequest<T>(
apiHost: string,
endpoint: string,
method: "GET" | "POST" | "PUT" | "DELETE",
data?: any
): Promise<Result<T, NetworkError | Error>> {
const url = new URL(endpoint, apiHost);
const body = JSON.stringify(data);
const res = wrapThrows(fetch)(url.toString(), {
method,
headers: {
"Content-Type": "application/json",
},
body,
});
if (res.ok === false) return err(res.error);
const response = await res.data;
const { data: innerData } = (await response.json()) as ApiResponse<T>;
if (!response.ok) {
return err({
code: "network_error",
message: response.statusText,
status: response.status,
url,
});
}
return ok(innerData as T);
}

View File

@@ -8,8 +8,6 @@ import { Logger } from "./lib/logger";
import { checkPageUrl } from "./lib/noCodeEvents";
import { resetPerson, setPersonAttribute, setPersonUserId, getPerson, logoutPerson } from "./lib/person";
export type { EnvironmentId, KeyValueData, PersonId, ResponseId, SurveyId } from "@formbricks/api";
const logger = Logger.getInstance();
logger.debug("Create command queue");

View File

@@ -1,4 +1,4 @@
import { FormbricksAPI, EnvironmentId } from "@formbricks/api";
import { FormbricksAPI } from "@formbricks/api";
import { Config } from "./config";
export const getApi = (): FormbricksAPI => {
@@ -11,6 +11,6 @@ export const getApi = (): FormbricksAPI => {
return new FormbricksAPI({
apiHost,
environmentId: environmentId as EnvironmentId,
environmentId,
});
};

View File

@@ -1,5 +1,8 @@
{
"extends": "@formbricks/tsconfig/base.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"composite": true
}
}

View File

@@ -70,6 +70,13 @@ class AuthorizationError extends Error {
}
}
type NetworkError = {
code: "network_error";
message: string;
status: number;
url: URL;
};
export {
ResourceNotFoundError,
InvalidInputError,
@@ -81,3 +88,4 @@ export {
AuthenticationError,
AuthorizationError,
};
export type { NetworkError };

17
pnpm-lock.yaml generated
View File

@@ -349,11 +349,10 @@ importers:
version: link:../../packages/eslint-config-formbricks
packages/api:
dependencies:
devDependencies:
'@formbricks/lib':
specifier: workspace:*
version: link:../lib
devDependencies:
'@formbricks/tsconfig':
specifier: workspace:*
version: link:../tsconfig
@@ -365,7 +364,10 @@ importers:
version: link:../eslint-config-formbricks
tsup:
specifier: ^7.2.0
version: 7.2.0
version: 7.2.0(typescript@5.1.6)
typescript:
specifier: 5.1.6
version: 5.1.6
packages/database:
dependencies:
@@ -21967,7 +21969,7 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
/tsup@7.2.0:
/tsup@7.2.0(typescript@5.1.6):
resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==}
engines: {node: '>=16.14'}
hasBin: true
@@ -21997,6 +21999,7 @@ packages:
source-map: 0.8.0-beta.0
sucrase: 3.32.0
tree-kill: 1.2.2
typescript: 5.1.6
transitivePeerDependencies:
- supports-color
- ts-node
@@ -22239,6 +22242,12 @@ packages:
hasBin: true
dev: true
/typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/typescript@5.2.2:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}