Refacto rest api, fix graphl playground, improve analytics (#5844)

- Improve the rest api by introducing startingAfter/endingBefore (we
previously had lastCursor), and moving pageInfo/totalCount outside of
the data object.
- Fix broken GraphQL playground on website
- Improve analytics by sending server url
This commit is contained in:
Félix Malfait
2024-06-12 21:54:33 +02:00
committed by GitHub
parent 04edf2bf7b
commit 374237a988
22 changed files with 277 additions and 131 deletions

View File

@ -2,11 +2,15 @@ import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
@Injectable()
export class TelemetryListener {
constructor(private readonly analyticsService: AnalyticsService) {}
constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
@OnEvent('*.created')
async handleAllCreate(payload: ObjectRecordCreateEvent<any>) {
@ -21,7 +25,7 @@ export class TelemetryListener {
payload.workspaceId,
'', // voluntarely not retrieving this
'', // to avoid slowing down
'',
this.environmentService.get('SERVER_URL'),
);
}
@ -38,7 +42,7 @@ export class TelemetryListener {
payload.workspaceId,
'',
'',
'',
this.environmentService.get('SERVER_URL'),
);
}
}

View File

@ -22,28 +22,28 @@ export class RestApiCoreController {
async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.get(request);
res.status(200).send(cleanGraphQLResponse(result.data));
res.status(200).send(cleanGraphQLResponse(result.data.data));
}
@Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.delete(request);
res.status(200).send(cleanGraphQLResponse(result.data));
res.status(200).send(cleanGraphQLResponse(result.data.data));
}
@Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.createOne(request);
res.status(201).send(cleanGraphQLResponse(result.data));
res.status(201).send(cleanGraphQLResponse(result.data.data));
}
@Patch()
async handleApiPatch(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
res.status(200).send(cleanGraphQLResponse(result.data.data));
}
// This endpoint is not documented in the OpenAPI schema.
@ -53,6 +53,6 @@ export class RestApiCoreController {
async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request);
res.status(200).send(cleanGraphQLResponse(result.data));
res.status(200).send(cleanGraphQLResponse(result.data.data));
}
}

View File

@ -7,11 +7,12 @@ import { DeleteVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-
import { CreateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-variables.factory';
import { UpdateVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/update-variables.factory';
import { GetVariablesFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/get-variables.factory';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-input.factory';
import { CreateManyQueryFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/create-many-query.factory';
import { StartingAfterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/starting-after-input.factory';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/ending-before-input.factory';
export const coreQueryBuilderFactories = [
DeleteQueryFactory,
@ -24,7 +25,8 @@ export const coreQueryBuilderFactories = [
CreateVariablesFactory,
UpdateVariablesFactory,
GetVariablesFactory,
LastCursorInputFactory,
StartingAfterInputFactory,
EndingBeforeInputFactory,
LimitInputFactory,
OrderByInputFactory,
FilterInputFactory,

View File

@ -15,11 +15,12 @@ export class FindManyQueryFactory {
query FindMany${capitalize(objectNamePlural)}(
$filter: ${objectNameSingular}FilterInput,
$orderBy: ${objectNameSingular}OrderByInput,
$lastCursor: String,
$startingAfter: String,
$endingBefore: String,
$limit: Int = 60
) {
${objectNamePlural}(
filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor
filter: $filter, orderBy: $orderBy, first: $limit, after: $startingAfter, before: $endingBefore
) {
edges {
node {

View File

@ -2,16 +2,18 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/limit-input.factory';
import { OrderByInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-input.factory';
import { QueryVariables } from 'src/engine/api/rest/types/query-variables.type';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/ending-before-input.factory';
import { StartingAfterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/starting-after-input.factory';
@Injectable()
export class GetVariablesFactory {
constructor(
private readonly lastCursorInputFactory: LastCursorInputFactory,
private readonly startingAfterInputFactory: StartingAfterInputFactory,
private readonly endingBeforeInputFactory: EndingBeforeInputFactory,
private readonly limitInputFactory: LimitInputFactory,
private readonly orderByInputFactory: OrderByInputFactory,
private readonly filterInputFactory: FilterInputFactory,
@ -30,7 +32,8 @@ export class GetVariablesFactory {
filter: this.filterInputFactory.create(request, objectMetadata),
orderBy: this.orderByInputFactory.create(request, objectMetadata),
limit: this.limitInputFactory.create(request),
lastCursor: this.lastCursorInputFactory.create(request),
startingAfter: this.startingAfterInputFactory.create(request),
endingBefore: this.endingBeforeInputFactory.create(request),
};
}
}

View File

@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/ending-before-input.factory';
describe('EndingBeforeInputFactory', () => {
let service: EndingBeforeInputFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EndingBeforeInputFactory],
}).compile();
service = module.get<EndingBeforeInputFactory>(EndingBeforeInputFactory);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should return default if ending_before missing', () => {
const request: any = { query: {} };
expect(service.create(request)).toEqual(undefined);
});
it('should return ending_before', () => {
const request: any = { query: { ending_before: 'uuid' } };
expect(service.create(request)).toEqual('uuid');
});
});
});

View File

@ -1,33 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LastCursorInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/last-cursor-input.factory';
describe('LastCursorInputFactory', () => {
let service: LastCursorInputFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LastCursorInputFactory],
}).compile();
service = module.get<LastCursorInputFactory>(LastCursorInputFactory);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should return default if last_cursor missing', () => {
const request: any = { query: {} };
expect(service.create(request)).toEqual(undefined);
});
it('should return last_cursor', () => {
const request: any = { query: { last_cursor: 'uuid' } };
expect(service.create(request)).toEqual('uuid');
});
});
});

View File

@ -0,0 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { StartingAfterInputFactory } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/starting-after-input.factory';
describe('StartingAfterInputFactory', () => {
let service: StartingAfterInputFactory;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [StartingAfterInputFactory],
}).compile();
service = module.get<StartingAfterInputFactory>(StartingAfterInputFactory);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should return default if starting_after missing', () => {
const request: any = { query: {} };
expect(service.create(request)).toEqual(undefined);
});
it('should return starting_after', () => {
const request: any = { query: { starting_after: 'uuid' } };
expect(service.create(request)).toEqual('uuid');
});
});
});

View File

@ -3,9 +3,9 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class LastCursorInputFactory {
export class EndingBeforeInputFactory {
create(request: Request): string | undefined {
const cursorQuery = request.query.last_cursor;
const cursorQuery = request.query.ending_before;
if (typeof cursorQuery !== 'string') {
return undefined;

View File

@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class StartingAfterInputFactory {
create(request: Request): string | undefined {
const cursorQuery = request.query.starting_after;
if (typeof cursorQuery !== 'string') {
return undefined;
}
return cursorQuery;
}
}

View File

@ -4,6 +4,7 @@ export type QueryVariables = {
filter?: object;
orderBy?: object;
limit?: number;
lastCursor?: string;
startingAfter?: string;
endingBefore?: string;
input?: object;
};

View File

@ -12,7 +12,9 @@ describe('cleanGraphQLResponse', () => {
},
};
const expectedResult = {
companies: [{ id: 'id', createdAt: '2023-01-01' }],
data: {
companies: [{ id: 'id', createdAt: '2023-01-01' }],
},
};
expect(cleanGraphQLResponse(data)).toEqual(expectedResult);
@ -20,6 +22,13 @@ describe('cleanGraphQLResponse', () => {
it('should remove nested edges/node from results', () => {
const data = {
companies: {
totalCount: 14,
pageInfo: {
hasNextPage: true,
startCursor:
'WyIwMDliYjNkYy1hNGEyLTRiNWUtYTZmYi1iMTFiMmFlMGI1MmIiXQ==',
endCursor: 'WyIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
edges: [
{
node: {
@ -34,20 +43,33 @@ describe('cleanGraphQLResponse', () => {
},
};
const expectedResult = {
companies: [
{
id: 'id',
createdAt: '2023-01-01',
people: [{ id: 'id1' }, { id: 'id2' }],
},
],
data: {
companies: [
{
id: 'id',
createdAt: '2023-01-01',
people: [{ id: 'id1' }, { id: 'id2' }],
},
],
},
totalCount: 14,
pageInfo: {
hasNextPage: true,
startCursor: 'WyIwMDliYjNkYy1hNGEyLTRiNWUtYTZmYi1iMTFiMmFlMGI1MmIiXQ==',
endCursor: 'WyIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
};
expect(cleanGraphQLResponse(data)).toEqual(expectedResult);
});
it('should not format when no list returned', () => {
const data = { company: { id: 'id' } };
const expectedResult = {
data: {
company: { id: 'id' },
},
};
expect(cleanGraphQLResponse(data)).toEqual(data);
expect(cleanGraphQLResponse(data)).toEqual(expectedResult);
});
});

View File

@ -1,16 +1,39 @@
// https://gist.github.com/ManUtopiK/469aec75b655d6a4d912aeb3b75af3c9
export const cleanGraphQLResponse = (input: any) => {
if (!input) return null;
const output = {};
const output = { data: {} }; // Initialize the output with a data key at the top level
const isObject = (obj: any) => {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
};
const cleanObject = (obj: any) => {
const cleanedObj = {};
Object.keys(obj).forEach((key) => {
if (isObject(obj[key])) {
if (obj[key].edges) {
// Handle edges by mapping over them and applying cleanObject to each node
cleanedObj[key] = obj[key].edges.map((edge) =>
cleanObject(edge.node),
);
} else {
// Recursively clean nested objects
cleanedObj[key] = cleanObject(obj[key]);
}
} else {
// Directly assign non-object properties
cleanedObj[key] = obj[key];
}
});
return cleanedObj;
};
Object.keys(input).forEach((key) => {
if (input[key] && input[key].edges) {
output[key] = input[key].edges.map((edge) =>
cleanGraphQLResponse(edge.node),
);
if (isObject(input[key]) && input[key].edges) {
// Handle collections with edges, ensuring data is placed under the data key
output.data[key] = input[key].edges.map((edge) => cleanObject(edge.node));
// Move pageInfo and totalCount to the top level
if (input[key].pageInfo) {
output['pageInfo'] = input[key].pageInfo;
}
@ -18,9 +41,11 @@ export const cleanGraphQLResponse = (input: any) => {
output['totalCount'] = input[key].totalCount;
}
} else if (isObject(input[key])) {
output[key] = cleanGraphQLResponse(input[key]);
} else if (key !== '__typename') {
output[key] = input[key];
// Recursively clean and assign nested objects under the data key
output.data[key] = cleanObject(input[key]);
} else {
// Assign all other properties directly under the data key
output.data[key] = input[key];
}
});

View File

@ -8,6 +8,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { AnalyticsService } from './analytics.service';
import { Analytics } from './analytics.entity';
@ -17,7 +18,10 @@ import { CreateAnalyticsInput } from './dtos/create-analytics.input';
@UseGuards(OptionalJwtAuthGuard)
@Resolver(() => Analytics)
export class AnalyticsResolver {
constructor(private readonly analyticsService: AnalyticsService) {}
constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
@Mutation(() => Analytics)
track(
@ -32,7 +36,7 @@ export class AnalyticsResolver {
workspace?.id,
workspace?.displayName,
workspace?.domainName,
request.hostname,
this.environmentService.get('SERVER_URL') ?? request.hostname,
);
}
}

View File

@ -2,11 +2,12 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder
import {
computeDepthParameters,
computeEndingBeforeParameters,
computeFilterParameters,
computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters,
computeOrderByParameters,
computeStartingAfterParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory';
import { FilterComparators } from 'src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
@ -106,13 +107,27 @@ describe('computeParameters', () => {
});
});
});
describe('computeLastCursor', () => {
it('should compute last cursor', () => {
expect(computeLastCursorParameters()).toEqual({
name: 'last_cursor',
describe('computeStartingAfter', () => {
it('should compute starting after', () => {
expect(computeStartingAfterParameters()).toEqual({
name: 'starting_after',
in: 'query',
description:
'Returns objects starting from a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
'Returns objects starting after a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
required: false,
schema: {
type: 'string',
},
});
});
});
describe('computeEndingBefore', () => {
it('should compute ending_before', () => {
expect(computeEndingBeforeParameters()).toEqual({
name: 'ending_before',
in: 'query',
description:
'Returns objects ending before a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
required: false,
schema: {
type: 'string',

View File

@ -5,11 +5,12 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
import { capitalize } from 'src/utils/capitalize';
import {
computeDepthParameters,
computeEndingBeforeParameters,
computeFilterParameters,
computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters,
computeOrderByParameters,
computeStartingAfterParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@ -223,7 +224,8 @@ export const computeParameterComponents = (): Record<
> => {
return {
idPath: computeIdPathParameter(),
lastCursor: computeLastCursorParameters(),
startingAfter: computeStartingAfterParameters(),
endingBefore: computeEndingBeforeParameters(),
filter: computeFilterParameters(),
depth: computeDepthParameters(),
orderBy: computeOrderByParameters(),

View File

@ -95,18 +95,33 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => {
};
};
export const computeLastCursorParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'last_cursor',
in: 'query',
description:
'Returns objects starting from a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
required: false,
schema: {
type: 'string',
},
export const computeStartingAfterParameters =
(): OpenAPIV3_1.ParameterObject => {
return {
name: 'starting_after',
in: 'query',
description:
'Returns objects starting after a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
required: false,
schema: {
type: 'string',
},
};
};
export const computeEndingBeforeParameters =
(): OpenAPIV3_1.ParameterObject => {
return {
name: 'ending_before',
in: 'query',
description:
'Returns objects ending before a specific cursor. You can find cursors in **startCursor** and **endCursor** in **pageInfo** in response data',
required: false,
schema: {
type: 'string',
},
};
};
};
export const computeIdPathParameter = (): OpenAPIV3_1.ParameterObject => {
return {

View File

@ -42,14 +42,15 @@ export const computeManyResultPath = (
get: {
tags: [item.namePlural],
summary: `Find Many ${item.namePlural}`,
description: `**order_by**, **filter**, **limit**, **depth** or **last_cursor** can be provided to request your **${item.namePlural}**`,
description: `**order_by**, **filter**, **limit**, **depth**, **starting_after** or **ending_before** can be provided to request your **${item.namePlural}**`,
operationId: `findMany${capitalize(item.namePlural)}`,
parameters: [
{ $ref: '#/components/parameters/orderBy' },
{ $ref: '#/components/parameters/filter' },
{ $ref: '#/components/parameters/limit' },
{ $ref: '#/components/parameters/depth' },
{ $ref: '#/components/parameters/lastCursor' },
{ $ref: '#/components/parameters/startingAfter' },
{ $ref: '#/components/parameters/endingBefore' },
],
responses: {
'200': getFindManyResponse200(item),

View File

@ -22,19 +22,19 @@ export const getFindManyResponse200 = (
)} with Relations`,
},
},
pageInfo: {
type: 'object',
properties: {
hasNextPage: { type: 'boolean' },
startCursor: { type: 'string' },
endCursor: { type: 'string' },
},
},
totalCount: {
type: 'integer',
},
},
},
pageInfo: {
type: 'object',
properties: {
hasNextPage: { type: 'boolean' },
startCursor: { type: 'string' },
endCursor: { type: 'string' },
},
},
totalCount: {
type: 'integer',
},
},
example: {
data: {
@ -44,6 +44,12 @@ export const getFindManyResponse200 = (
'...',
],
},
pageInfo: {
hasNextPage: true,
startCursor: '56f411fb-0900-4ffb-b942-d7e8d6709eff',
endCursor: '93adf3c6-6cf7-4a86-adcd-75f77857ba67',
},
totalCount: 132,
},
},
},

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
type MessagingTelemetryTrackInput = {
eventName: string;
@ -13,7 +14,10 @@ type MessagingTelemetryTrackInput = {
@Injectable()
export class MessagingTelemetryService {
constructor(private readonly analyticsService: AnalyticsService) {}
constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
public async track({
eventName,
@ -39,7 +43,7 @@ export class MessagingTelemetryService {
workspaceId,
'', // voluntarely not retrieving this
'', // to avoid slowing down
'',
this.environmentService.get('SERVER_URL'),
);
}
}

View File

@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import { Theme, useTheme } from '@graphiql/react';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
@ -9,6 +9,13 @@ import { SubDoc } from '@/app/_components/playground/token-form';
import Playground from './playground';
import 'graphiql/graphiql.css';
import '@graphiql/plugin-explorer/dist/style.css';
const StyledContainer = styled.div`
height: 100%;
`;
const SubDocToPath = {
core: 'graphql',
metadata: 'metadata',
@ -28,37 +35,19 @@ const GraphQlComponent = ({ token, baseUrl, path }: any) => {
}
return (
<div className="fullHeightPlayground">
<StyledContainer>
<GraphiQL
plugins={[explorer]}
fetcher={fetcher}
defaultHeaders={JSON.stringify({ Authorization: `Bearer ${token}` })}
/>
</div>
</StyledContainer>
);
};
const GraphQlPlayground = ({ subDoc }: { subDoc: SubDoc }) => {
const [token, setToken] = useState<string>();
const [baseUrl, setBaseUrl] = useState<string>();
const { setTheme } = useTheme();
useEffect(() => {
window.localStorage.setItem(
'graphiql:theme',
window.localStorage.getItem('theme') || 'light',
);
const handleThemeChange = (ev: any) => {
if (ev.key === 'theme') {
setTheme(ev.newValue as Theme);
}
};
window.addEventListener('storage', handleThemeChange);
return () => window.removeEventListener('storage', handleThemeChange);
}, []);
const children = (
<GraphQlComponent

View File

@ -33,20 +33,23 @@ const TokenForm = ({
const [isLoading, setIsLoading] = useState(false);
const [locationSetting, setLocationSetting] = useState(
(window &&
(typeof window !== 'undefined' &&
window.localStorage.getItem('baseUrl') &&
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')
?.locationSetting) ??
'production',
);
const [baseUrl, setBaseUrl] = useState(
(window.localStorage.getItem('baseUrl') &&
(typeof window !== 'undefined' &&
window.localStorage.getItem('baseUrl') &&
JSON.parse(window.localStorage.getItem('baseUrl') ?? '')?.baseUrl) ??
'https://api.twenty.com',
);
const tokenLocal = window?.localStorage?.getItem?.(
'TryIt_securitySchemeValues',
const tokenLocal = (
typeof window !== 'undefined'
? window?.localStorage?.getItem?.('TryIt_securitySchemeValues')
: '{}'
) as string;
const token = JSON.parse(tokenLocal)?.bearerAuth ?? '';