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

View File

@ -22,28 +22,28 @@ export class RestApiCoreController {
async handleApiGet(@Req() request: Request, @Res() res: Response) { async handleApiGet(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.get(request); const result = await this.restApiCoreService.get(request);
res.status(200).send(cleanGraphQLResponse(result.data)); res.status(200).send(cleanGraphQLResponse(result.data.data));
} }
@Delete() @Delete()
async handleApiDelete(@Req() request: Request, @Res() res: Response) { async handleApiDelete(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.delete(request); const result = await this.restApiCoreService.delete(request);
res.status(200).send(cleanGraphQLResponse(result.data)); res.status(200).send(cleanGraphQLResponse(result.data.data));
} }
@Post() @Post()
async handleApiPost(@Req() request: Request, @Res() res: Response) { async handleApiPost(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.createOne(request); const result = await this.restApiCoreService.createOne(request);
res.status(201).send(cleanGraphQLResponse(result.data)); res.status(201).send(cleanGraphQLResponse(result.data.data));
} }
@Patch() @Patch()
async handleApiPatch(@Req() request: Request, @Res() res: Response) { async handleApiPatch(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request); 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. // This endpoint is not documented in the OpenAPI schema.
@ -53,6 +53,6 @@ export class RestApiCoreController {
async handleApiPut(@Req() request: Request, @Res() res: Response) { async handleApiPut(@Req() request: Request, @Res() res: Response) {
const result = await this.restApiCoreService.update(request); 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 = [ export const coreQueryBuilderFactories = [
DeleteQueryFactory, DeleteQueryFactory,
@ -24,7 +25,8 @@ export const coreQueryBuilderFactories = [
CreateVariablesFactory, CreateVariablesFactory,
UpdateVariablesFactory, UpdateVariablesFactory,
GetVariablesFactory, GetVariablesFactory,
LastCursorInputFactory, StartingAfterInputFactory,
EndingBeforeInputFactory,
LimitInputFactory, LimitInputFactory,
OrderByInputFactory, OrderByInputFactory,
FilterInputFactory, FilterInputFactory,

View File

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

View File

@ -2,16 +2,18 @@ import { Injectable } from '@nestjs/common';
import { Request } from 'express'; 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 { 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 { 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 { 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 { 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() @Injectable()
export class GetVariablesFactory { export class GetVariablesFactory {
constructor( constructor(
private readonly lastCursorInputFactory: LastCursorInputFactory, private readonly startingAfterInputFactory: StartingAfterInputFactory,
private readonly endingBeforeInputFactory: EndingBeforeInputFactory,
private readonly limitInputFactory: LimitInputFactory, private readonly limitInputFactory: LimitInputFactory,
private readonly orderByInputFactory: OrderByInputFactory, private readonly orderByInputFactory: OrderByInputFactory,
private readonly filterInputFactory: FilterInputFactory, private readonly filterInputFactory: FilterInputFactory,
@ -30,7 +32,8 @@ export class GetVariablesFactory {
filter: this.filterInputFactory.create(request, objectMetadata), filter: this.filterInputFactory.create(request, objectMetadata),
orderBy: this.orderByInputFactory.create(request, objectMetadata), orderBy: this.orderByInputFactory.create(request, objectMetadata),
limit: this.limitInputFactory.create(request), 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'; import { Request } from 'express';
@Injectable() @Injectable()
export class LastCursorInputFactory { export class EndingBeforeInputFactory {
create(request: Request): string | undefined { create(request: Request): string | undefined {
const cursorQuery = request.query.last_cursor; const cursorQuery = request.query.ending_before;
if (typeof cursorQuery !== 'string') { if (typeof cursorQuery !== 'string') {
return undefined; 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; filter?: object;
orderBy?: object; orderBy?: object;
limit?: number; limit?: number;
lastCursor?: string; startingAfter?: string;
endingBefore?: string;
input?: object; input?: object;
}; };

View File

@ -12,7 +12,9 @@ describe('cleanGraphQLResponse', () => {
}, },
}; };
const expectedResult = { const expectedResult = {
companies: [{ id: 'id', createdAt: '2023-01-01' }], data: {
companies: [{ id: 'id', createdAt: '2023-01-01' }],
},
}; };
expect(cleanGraphQLResponse(data)).toEqual(expectedResult); expect(cleanGraphQLResponse(data)).toEqual(expectedResult);
@ -20,6 +22,13 @@ describe('cleanGraphQLResponse', () => {
it('should remove nested edges/node from results', () => { it('should remove nested edges/node from results', () => {
const data = { const data = {
companies: { companies: {
totalCount: 14,
pageInfo: {
hasNextPage: true,
startCursor:
'WyIwMDliYjNkYy1hNGEyLTRiNWUtYTZmYi1iMTFiMmFlMGI1MmIiXQ==',
endCursor: 'WyIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
edges: [ edges: [
{ {
node: { node: {
@ -34,20 +43,33 @@ describe('cleanGraphQLResponse', () => {
}, },
}; };
const expectedResult = { const expectedResult = {
companies: [ data: {
{ companies: [
id: 'id', {
createdAt: '2023-01-01', id: 'id',
people: [{ id: 'id1' }, { id: 'id2' }], createdAt: '2023-01-01',
}, people: [{ id: 'id1' }, { id: 'id2' }],
], },
],
},
totalCount: 14,
pageInfo: {
hasNextPage: true,
startCursor: 'WyIwMDliYjNkYy1hNGEyLTRiNWUtYTZmYi1iMTFiMmFlMGI1MmIiXQ==',
endCursor: 'WyIyMDIwMjAyMC0wNzEzLTQwYTUtODIxNi04MjgwMjQwMWQzM2UiXQ==',
},
}; };
expect(cleanGraphQLResponse(data)).toEqual(expectedResult); expect(cleanGraphQLResponse(data)).toEqual(expectedResult);
}); });
it('should not format when no list returned', () => { it('should not format when no list returned', () => {
const data = { company: { id: 'id' } }; 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) => { export const cleanGraphQLResponse = (input: any) => {
if (!input) return null; if (!input) return null;
const output = {}; const output = { data: {} }; // Initialize the output with a data key at the top level
const isObject = (obj: any) => { const isObject = (obj: any) => {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj); 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) => { Object.keys(input).forEach((key) => {
if (input[key] && input[key].edges) { if (isObject(input[key]) && input[key].edges) {
output[key] = input[key].edges.map((edge) => // Handle collections with edges, ensuring data is placed under the data key
cleanGraphQLResponse(edge.node), output.data[key] = input[key].edges.map((edge) => cleanObject(edge.node));
); // Move pageInfo and totalCount to the top level
if (input[key].pageInfo) { if (input[key].pageInfo) {
output['pageInfo'] = input[key].pageInfo; output['pageInfo'] = input[key].pageInfo;
} }
@ -18,9 +41,11 @@ export const cleanGraphQLResponse = (input: any) => {
output['totalCount'] = input[key].totalCount; output['totalCount'] = input[key].totalCount;
} }
} else if (isObject(input[key])) { } else if (isObject(input[key])) {
output[key] = cleanGraphQLResponse(input[key]); // Recursively clean and assign nested objects under the data key
} else if (key !== '__typename') { output.data[key] = cleanObject(input[key]);
output[key] = 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 { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.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 { AnalyticsService } from './analytics.service';
import { Analytics } from './analytics.entity'; import { Analytics } from './analytics.entity';
@ -17,7 +18,10 @@ import { CreateAnalyticsInput } from './dtos/create-analytics.input';
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Resolver(() => Analytics) @Resolver(() => Analytics)
export class AnalyticsResolver { export class AnalyticsResolver {
constructor(private readonly analyticsService: AnalyticsService) {} constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
@Mutation(() => Analytics) @Mutation(() => Analytics)
track( track(
@ -32,7 +36,7 @@ export class AnalyticsResolver {
workspace?.id, workspace?.id,
workspace?.displayName, workspace?.displayName,
workspace?.domainName, 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 { import {
computeDepthParameters, computeDepthParameters,
computeEndingBeforeParameters,
computeFilterParameters, computeFilterParameters,
computeIdPathParameter, computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters, computeLimitParameters,
computeOrderByParameters, computeOrderByParameters,
computeStartingAfterParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils'; } 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 { 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'; 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', () => { describe('computeStartingAfter', () => {
it('should compute last cursor', () => { it('should compute starting after', () => {
expect(computeLastCursorParameters()).toEqual({ expect(computeStartingAfterParameters()).toEqual({
name: 'last_cursor', name: 'starting_after',
in: 'query', in: 'query',
description: 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, required: false,
schema: { schema: {
type: 'string', 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 { capitalize } from 'src/utils/capitalize';
import { import {
computeDepthParameters, computeDepthParameters,
computeEndingBeforeParameters,
computeFilterParameters, computeFilterParameters,
computeIdPathParameter, computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters, computeLimitParameters,
computeOrderByParameters, computeOrderByParameters,
computeStartingAfterParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils'; } from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@ -223,7 +224,8 @@ export const computeParameterComponents = (): Record<
> => { > => {
return { return {
idPath: computeIdPathParameter(), idPath: computeIdPathParameter(),
lastCursor: computeLastCursorParameters(), startingAfter: computeStartingAfterParameters(),
endingBefore: computeEndingBeforeParameters(),
filter: computeFilterParameters(), filter: computeFilterParameters(),
depth: computeDepthParameters(), depth: computeDepthParameters(),
orderBy: computeOrderByParameters(), orderBy: computeOrderByParameters(),

View File

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

View File

@ -42,14 +42,15 @@ export const computeManyResultPath = (
get: { get: {
tags: [item.namePlural], tags: [item.namePlural],
summary: `Find Many ${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)}`, operationId: `findMany${capitalize(item.namePlural)}`,
parameters: [ parameters: [
{ $ref: '#/components/parameters/orderBy' }, { $ref: '#/components/parameters/orderBy' },
{ $ref: '#/components/parameters/filter' }, { $ref: '#/components/parameters/filter' },
{ $ref: '#/components/parameters/limit' }, { $ref: '#/components/parameters/limit' },
{ $ref: '#/components/parameters/depth' }, { $ref: '#/components/parameters/depth' },
{ $ref: '#/components/parameters/lastCursor' }, { $ref: '#/components/parameters/startingAfter' },
{ $ref: '#/components/parameters/endingBefore' },
], ],
responses: { responses: {
'200': getFindManyResponse200(item), '200': getFindManyResponse200(item),

View File

@ -22,19 +22,19 @@ export const getFindManyResponse200 = (
)} with Relations`, )} 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: { example: {
data: { 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 { Injectable } from '@nestjs/common';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
type MessagingTelemetryTrackInput = { type MessagingTelemetryTrackInput = {
eventName: string; eventName: string;
@ -13,7 +14,10 @@ type MessagingTelemetryTrackInput = {
@Injectable() @Injectable()
export class MessagingTelemetryService { export class MessagingTelemetryService {
constructor(private readonly analyticsService: AnalyticsService) {} constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
public async track({ public async track({
eventName, eventName,
@ -39,7 +43,7 @@ export class MessagingTelemetryService {
workspaceId, workspaceId,
'', // voluntarely not retrieving this '', // voluntarely not retrieving this
'', // to avoid slowing down '', // to avoid slowing down
'', this.environmentService.get('SERVER_URL'),
); );
} }
} }

View File

@ -1,7 +1,7 @@
'use client'; '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 { explorerPlugin } from '@graphiql/plugin-explorer';
import { Theme, useTheme } from '@graphiql/react';
import { createGraphiQLFetcher } from '@graphiql/toolkit'; import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql'; import { GraphiQL } from 'graphiql';
@ -9,6 +9,13 @@ import { SubDoc } from '@/app/_components/playground/token-form';
import Playground from './playground'; import Playground from './playground';
import 'graphiql/graphiql.css';
import '@graphiql/plugin-explorer/dist/style.css';
const StyledContainer = styled.div`
height: 100%;
`;
const SubDocToPath = { const SubDocToPath = {
core: 'graphql', core: 'graphql',
metadata: 'metadata', metadata: 'metadata',
@ -28,37 +35,19 @@ const GraphQlComponent = ({ token, baseUrl, path }: any) => {
} }
return ( return (
<div className="fullHeightPlayground"> <StyledContainer>
<GraphiQL <GraphiQL
plugins={[explorer]} plugins={[explorer]}
fetcher={fetcher} fetcher={fetcher}
defaultHeaders={JSON.stringify({ Authorization: `Bearer ${token}` })} defaultHeaders={JSON.stringify({ Authorization: `Bearer ${token}` })}
/> />
</div> </StyledContainer>
); );
}; };
const GraphQlPlayground = ({ subDoc }: { subDoc: SubDoc }) => { const GraphQlPlayground = ({ subDoc }: { subDoc: SubDoc }) => {
const [token, setToken] = useState<string>(); const [token, setToken] = useState<string>();
const [baseUrl, setBaseUrl] = 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 = ( const children = (
<GraphQlComponent <GraphQlComponent

View File

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