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 { 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@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;
|
||||
@ -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;
|
||||
orderBy?: object;
|
||||
limit?: number;
|
||||
lastCursor?: string;
|
||||
startingAfter?: string;
|
||||
endingBefore?: string;
|
||||
input?: object;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user