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:
@ -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'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user