feat: wip server folder structure (#4573)

* feat: wip server folder structure

* fix: merge

* fix: wrong merge

* fix: remove unused file

* fix: comment

* fix: lint

* fix: merge

* fix: remove console.log

* fix: metadata graphql arguments broken
This commit is contained in:
Jérémy M
2024-03-20 16:23:46 +01:00
committed by GitHub
parent da12710fe9
commit e5c1309e8c
461 changed files with 1396 additions and 1322 deletions

View File

@ -0,0 +1,30 @@
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service';
@Controller('open-api')
export class OpenApiController {
constructor(private readonly openApiService: OpenApiService) {}
@Get('core')
async generateOpenApiSchemaCore(
@Req() request: Request,
@Res() res: Response,
) {
const data = await this.openApiService.generateCoreSchema(request);
res.send(data);
}
@Get('metadata')
async generateOpenApiSchemaMetaData(
@Req() request: Request,
@Res() res: Response,
) {
const data = await this.openApiService.generateMetaDataSchema(request);
res.send(data);
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { OpenApiController } from 'src/engine/core-modules/open-api/open-api.controller';
import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
@Module({
imports: [ObjectMetadataModule, AuthModule],
controllers: [OpenApiController],
providers: [OpenApiService],
})
export class OpenApiModule {}

View File

@ -0,0 +1,35 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
describe('OpenApiService', () => {
let service: OpenApiService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
OpenApiService,
{
provide: TokenService,
useValue: {},
},
{
provide: ObjectMetadataService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
],
}).compile();
service = module.get<OpenApiService>(OpenApiService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,199 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { OpenAPIV3_1 } from 'openapi-types';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { baseSchema } from 'src/engine/core-modules/open-api/utils/base-schema.utils';
import {
computeManyResultPath,
computeSingleResultPath,
} from 'src/engine/core-modules/open-api/utils/path.utils';
import { getErrorResponses } from 'src/engine/core-modules/open-api/utils/get-error-responses.utils';
import {
computeMetadataSchemaComponents,
computeParameterComponents,
computeSchemaComponents,
} from 'src/engine/core-modules/open-api/utils/components.utils';
import { computeSchemaTags } from 'src/engine/core-modules/open-api/utils/compute-schema-tags.utils';
import { computeWebhooks } from 'src/engine/core-modules/open-api/utils/computeWebhooks.utils';
import { capitalize } from 'src/utils/capitalize';
import {
getDeleteResponse200,
getManyResultResponse200,
getSingleResultSuccessResponse,
} from 'src/engine/core-modules/open-api/utils/responses.utils';
import { getRequestBody } from 'src/engine/core-modules/open-api/utils/request-body.utils';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { getServerUrl } from 'src/utils/get-server-url';
@Injectable()
export class OpenApiService {
constructor(
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
private readonly objectMetadataService: ObjectMetadataService,
) {}
async generateCoreSchema(request: Request): Promise<OpenAPIV3_1.Document> {
const baseUrl = getServerUrl(
request,
this.environmentService.get('SERVER_URL'),
);
const schema = baseSchema('core', baseUrl);
let objectMetadataItems;
try {
const { workspace } = await this.tokenService.validateToken(request);
objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
} catch (err) {
return schema;
}
if (!objectMetadataItems.length) {
return schema;
}
schema.paths = objectMetadataItems.reduce((paths, item) => {
paths[`/${item.namePlural}`] = computeManyResultPath(item);
paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item);
return paths;
}, schema.paths as OpenAPIV3_1.PathsObject);
schema.webhooks = objectMetadataItems.reduce(
(paths, item) => {
paths[`Create ${item.nameSingular}`] = computeWebhooks('create', item);
paths[`Update ${item.nameSingular}`] = computeWebhooks('update', item);
paths[`Delete ${item.nameSingular}`] = computeWebhooks('delete', item);
return paths;
},
{} as Record<
string,
OpenAPIV3_1.PathItemObject | OpenAPIV3_1.ReferenceObject
>,
);
schema.tags = computeSchemaTags(objectMetadataItems);
schema.components = {
...schema.components, // components.securitySchemes is defined in base Schema
schemas: computeSchemaComponents(objectMetadataItems),
parameters: computeParameterComponents(),
responses: {
'400': getErrorResponses('Invalid request'),
'401': getErrorResponses('Unauthorized'),
},
};
return schema;
}
async generateMetaDataSchema(
request: Request,
): Promise<OpenAPIV3_1.Document> {
const baseUrl = getServerUrl(
request,
this.environmentService.get('SERVER_URL'),
);
const schema = baseSchema('metadata', baseUrl);
schema.tags = [{ name: 'placeholder' }];
const metadata = [
{
nameSingular: 'object',
namePlural: 'objects',
},
{
nameSingular: 'field',
namePlural: 'fields',
},
{
nameSingular: 'relation',
namePlural: 'relations',
},
];
schema.paths = metadata.reduce((path, item) => {
path[`/${item.namePlural}`] = {
get: {
tags: [item.namePlural],
summary: `Find Many ${item.namePlural}`,
parameters: [{ $ref: '#/components/parameters/filter' }],
responses: {
'200': getManyResultResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
post: {
tags: [item.namePlural],
summary: `Create One ${item.nameSingular}`,
operationId: `createOne${capitalize(item.nameSingular)}`,
requestBody: getRequestBody(item),
responses: {
'200': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
} as OpenAPIV3_1.PathItemObject;
path[`/${item.namePlural}/{id}`] = {
get: {
tags: [item.namePlural],
summary: `Find One ${item.nameSingular}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
responses: {
'200': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
delete: {
tags: [item.namePlural],
summary: `Delete One ${item.nameSingular}`,
operationId: `deleteOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
responses: {
'200': getDeleteResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
put: {
tags: [item.namePlural],
summary: `Update One ${item.namePlural}`,
operationId: `updateOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
requestBody: getRequestBody(item),
responses: {
'200': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
} as OpenAPIV3_1.PathItemObject;
return path;
}, schema.paths as OpenAPIV3_1.PathsObject);
schema.components = {
...schema.components, // components.securitySchemes is defined in base Schema
schemas: computeMetadataSchemaComponents(metadata),
parameters: computeParameterComponents(),
responses: {
'400': getErrorResponses('Invalid request'),
'401': getErrorResponses('Unauthorized'),
},
};
return schema;
}
}

View File

@ -0,0 +1,41 @@
import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils';
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
describe('computeSchemaComponents', () => {
it('should compute schema components', () => {
expect(
computeSchemaComponents([
objectMetadataItemMock,
] as ObjectMetadataEntity[]),
).toEqual({
ObjectName: {
type: 'object',
required: ['fieldNumber'],
example: { fieldNumber: '' },
properties: {
fieldCurrency: {
properties: {
amountMicros: { type: 'string' },
currencyCode: { type: 'string' },
},
type: 'object',
},
fieldLink: {
properties: {
label: { type: 'string' },
url: { type: 'string' },
},
type: 'object',
},
fieldNumber: {
type: 'number',
},
fieldString: {
type: 'string',
},
},
},
});
});
});

View File

@ -0,0 +1,136 @@
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import {
computeDepthParameters,
computeFilterParameters,
computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters,
computeOrderByParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { FilterComparators } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
describe('computeParameters', () => {
describe('computeLimit', () => {
it('should compute limit', () => {
expect(computeLimitParameters()).toEqual({
name: 'limit',
in: 'query',
description: 'Limits the number of objects returned.',
required: false,
schema: {
type: 'integer',
minimum: 0,
maximum: 60,
default: 60,
},
});
});
});
describe('computeOrderBy', () => {
it('should compute order by', () => {
expect(computeOrderByParameters()).toEqual({
name: 'order_by',
in: 'query',
description: `Sorts objects returned.
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
Available directions are **${Object.values(OrderByDirection).join(
'**, **',
)}**.
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
required: false,
schema: {
type: 'string',
},
examples: {
simple: {
value: 'createdAt',
summary: 'A simple order_by param',
},
complex: {
value: `id[${OrderByDirection.AscNullsFirst}],createdAt[${OrderByDirection.DescNullsLast}]`,
summary: 'A more complex order_by param',
},
},
});
});
});
describe('computeDepth', () => {
it('should compute depth', () => {
expect(computeDepthParameters()).toEqual({
name: 'depth',
in: 'query',
description: 'Limits the depth objects returned.',
required: false,
schema: {
type: 'integer',
enum: [1, 2],
},
});
});
});
describe('computeFilter', () => {
it('should compute filters', () => {
expect(computeFilterParameters()).toEqual({
name: 'filter',
in: 'query',
description: `Filters objects returned.
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2,...**
Available comparators are **${Object.values(FilterComparators).join(
'**, **',
)}**.
You can create more complex filters using conjunctions **${Object.values(
Conjunctions,
).join('**, **')}**.
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
required: false,
schema: {
type: 'string',
},
examples: {
simple: {
value: 'createdAt[gte]:"2023-01-01"',
description: 'A simple filter param',
},
complex: {
value:
'or(createdAt[gte]:"2024-01-01",createdAt[lte]:"2023-01-01",not(id[is]:NULL))',
description: 'A more complex filter param',
},
},
});
});
});
describe('computeLastCursor', () => {
it('should compute last cursor', () => {
expect(computeLastCursorParameters()).toEqual({
name: 'last_cursor',
in: 'query',
description: 'Returns objects starting from a specific cursor.',
required: false,
schema: {
type: 'string',
},
});
});
});
describe('computeIdPathParameter', () => {
it('should compute id path param', () => {
expect(computeIdPathParameter()).toEqual({
name: 'id',
in: 'path',
description: 'Object id.',
required: true,
schema: {
type: 'string',
format: 'uuid',
},
});
});
});
});

View File

@ -0,0 +1,53 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { computeOpenApiPath } from 'src/engine/core-modules/open-api/utils/path.utils';
export const baseSchema = (
schemaName: 'core' | 'metadata',
serverUrl: string,
): OpenAPIV3_1.Document => {
return {
openapi: '3.0.3',
info: {
title: 'Twenty Api',
description: `This is a **Twenty REST/API** playground based on the **OpenAPI 3.0 specification**.`,
termsOfService: 'https://github.com/twentyhq/twenty?tab=coc-ov-file',
contact: {
email: 'felix@twenty.com',
},
license: {
name: 'AGPL-3.0',
url: 'https://github.com/twentyhq/twenty?tab=AGPL-3.0-1-ov-file#readme',
},
version: '0.2.0',
},
// Testing purposes
servers: [
{
url: `${serverUrl}/rest/${schemaName !== 'core' ? schemaName : ''}`,
description: 'Production Development',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description:
'Enter the token with the `Bearer: ` prefix, e.g. "Bearer abcde12345".',
},
},
},
security: [
{
bearerAuth: [],
},
],
externalDocs: {
description: 'Find out more about **Twenty**',
url: 'https://twenty.com',
},
paths: { [`/open-api/${schemaName}`]: computeOpenApiPath(serverUrl) },
};
};

View File

@ -0,0 +1,299 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
import {
computeDepthParameters,
computeFilterParameters,
computeIdPathParameter,
computeLastCursorParameters,
computeLimitParameters,
computeOrderByParameters,
} from 'src/engine/core-modules/open-api/utils/parameters.utils';
type Property = OpenAPIV3_1.SchemaObject;
type Properties = {
[name: string]: Property;
};
const getSchemaComponentsProperties = (
item: ObjectMetadataEntity,
): Properties => {
return item.fields.reduce((node, field) => {
let itemProperty = {} as Property;
switch (field.type) {
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.DATE_TIME:
itemProperty.type = 'string';
break;
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
case FieldMetadataType.PROBABILITY:
case FieldMetadataType.RATING:
case FieldMetadataType.POSITION:
itemProperty.type = 'number';
break;
case FieldMetadataType.BOOLEAN:
itemProperty.type = 'boolean';
break;
case FieldMetadataType.RELATION:
if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) {
itemProperty = {
type: 'array',
items: {
$ref: `#/components/schemas/${capitalize(
field.fromRelationMetadata?.toObjectMetadata.nameSingular || '',
)}`,
},
};
}
break;
case FieldMetadataType.LINK:
case FieldMetadataType.CURRENCY:
case FieldMetadataType.FULL_NAME:
itemProperty = {
type: 'object',
properties: Object.keys(field.targetColumnMap).reduce(
(properties, key) => {
properties[key] = { type: 'string' };
return properties;
},
{} as Properties,
),
};
break;
case FieldMetadataType.JSON:
type: 'object';
break;
default:
itemProperty.type = 'string';
break;
}
if (field.description) {
itemProperty.description = field.description;
}
if (Object.keys(itemProperty).length) {
node[field.name] = itemProperty;
}
return node;
}, {} as Properties);
};
const getRequiredFields = (item: ObjectMetadataEntity): string[] => {
return item.fields.reduce((required, field) => {
if (!field.isNullable && field.defaultValue === null) {
required.push(field.name);
return required;
}
return required;
}, [] as string[]);
};
const computeSchemaComponent = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.SchemaObject => {
const result = {
type: 'object',
description: item.description,
properties: getSchemaComponentsProperties(item),
example: {},
} as OpenAPIV3_1.SchemaObject;
const requiredFields = getRequiredFields(item);
if (requiredFields?.length) {
result.required = requiredFields;
result.example = requiredFields.reduce(
(example, requiredField) => {
example[requiredField] = '';
return example;
},
{} as Record<string, string>,
);
}
return result;
};
export const computeSchemaComponents = (
objectMetadataItems: ObjectMetadataEntity[],
): Record<string, OpenAPIV3_1.SchemaObject> => {
return objectMetadataItems.reduce(
(schemas, item) => {
schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item);
return schemas;
},
{} as Record<string, OpenAPIV3_1.SchemaObject>,
);
};
export const computeParameterComponents = (): Record<
string,
OpenAPIV3_1.ParameterObject
> => {
return {
idPath: computeIdPathParameter(),
lastCursor: computeLastCursorParameters(),
filter: computeFilterParameters(),
depth: computeDepthParameters(),
orderBy: computeOrderByParameters(),
limit: computeLimitParameters(),
};
};
export const computeMetadataSchemaComponents = (
metadataSchema: { nameSingular: string; namePlural: string }[],
): Record<string, OpenAPIV3_1.SchemaObject> => {
return metadataSchema.reduce(
(schemas, item) => {
switch (item.nameSingular) {
case 'object': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
properties: {
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
namePlural: { type: 'string' },
labelSingular: { type: 'string' },
labelPlural: { type: 'string' },
description: { type: 'string' },
icon: { type: 'string' },
isCustom: { type: 'boolean' },
isActive: { type: 'boolean' },
isSystem: { type: 'boolean' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
labelIdentifierFieldMetadataId: { type: 'string' },
imageIdentifierFieldMetadataId: { type: 'string' },
fields: {
type: 'object',
properties: {
edges: {
type: 'object',
properties: {
node: {
type: 'array',
items: {
$ref: '#/components/schemas/Field',
},
},
},
},
},
},
},
};
return schemas;
}
case 'field': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
properties: {
type: { type: 'string' },
name: { type: 'string' },
label: { type: 'string' },
description: { type: 'string' },
icon: { type: 'string' },
isCustom: { type: 'boolean' },
isActive: { type: 'boolean' },
isSystem: { type: 'boolean' },
isNullable: { type: 'boolean' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
fromRelationMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
relationType: { type: 'string' },
toObjectMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
namePlural: { type: 'string' },
isSystem: { type: 'boolean' },
},
},
toFieldMetadataId: { type: 'string' },
},
},
toRelationMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
relationType: { type: 'string' },
fromObjectMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
namePlural: { type: 'string' },
isSystem: { type: 'boolean' },
},
},
fromFieldMetadataId: { type: 'string' },
},
},
defaultValue: { type: 'object' },
options: { type: 'object' },
},
};
return schemas;
}
case 'relation': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
properties: {
relationType: { type: 'string' },
fromObjectMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
namePlural: { type: 'string' },
isSystem: { type: 'boolean' },
},
},
fromObjectMetadataId: { type: 'string' },
toObjectMetadata: {
type: 'object',
properties: {
id: { type: 'string' },
dataSourceId: { type: 'string' },
nameSingular: { type: 'string' },
namePlural: { type: 'string' },
isSystem: { type: 'boolean' },
},
},
toObjectMetadataId: { type: 'string' },
fromFieldMetadataId: { type: 'string' },
toFieldMetadataId: { type: 'string' },
},
};
}
}
return schemas;
},
{} as Record<string, OpenAPIV3_1.SchemaObject>,
);
};

View File

@ -0,0 +1,19 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const computeSchemaTags = (
items: ObjectMetadataEntity[],
): OpenAPIV3_1.TagObject[] => {
const results = [{ name: 'General', description: 'General requests' }];
items.forEach((item) => {
results.push({
name: item.namePlural,
description: `Object \`${capitalize(item.namePlural)}\``,
});
});
return results;
};

View File

@ -0,0 +1,74 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const computeWebhooks = (
type: 'create' | 'update' | 'delete',
item: ObjectMetadataEntity,
): OpenAPIV3_1.PathItemObject => {
return {
post: {
tags: [item.nameSingular],
security: [],
requestBody: {
description: `*${type}*.**${item.nameSingular}**, *&#42;*.**${item.nameSingular}**, *&#42;*.**&#42;**`,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
targetUrl: {
type: 'string',
example: 'https://example.com/incomingWebhook',
},
eventType: {
type: 'string',
enum: [
'*.*',
'*.' + item.nameSingular,
type + '.' + item.nameSingular,
],
},
objectMetadata: {
type: 'object',
properties: {
id: {
type: 'string',
example: '370985db-22d8-4463-8e5f-2271d30913bd',
},
nameSingular: {
type: 'string',
enum: [item.nameSingular],
},
},
},
workspaceId: {
type: 'string',
example: '872cfcf1-c79f-42bc-877d-5829f06eb3f9',
},
webhookId: {
type: 'string',
example: '90056586-1228-4e03-a507-70140aa85c05',
},
eventDate: {
type: 'string',
example: '2024-02-14T11:27:01.779Z',
},
record: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
},
},
},
},
},
responses: {
'200': {
description:
'Return a 200 status to indicate that the data was received successfully',
},
},
},
} as OpenAPIV3_1.PathItemObject;
};

View File

@ -0,0 +1,19 @@
import { OpenAPIV3_1 } from 'openapi-types';
export const getErrorResponses = (
description: string,
): OpenAPIV3_1.ResponseObject => {
return {
description,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
},
},
};
};

View File

@ -0,0 +1,121 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FilterComparators } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/parse-filter.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/order-by-input.factory';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/api-rest-query-builder/factories/input-factories/filter-utils/add-default-conjunction.utils';
export const computeLimitParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'limit',
in: 'query',
description: 'Limits the number of objects returned.',
required: false,
schema: {
type: 'integer',
minimum: 0,
maximum: 60,
default: 60,
},
};
};
export const computeOrderByParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'order_by',
in: 'query',
description: `Sorts objects returned.
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
Available directions are **${Object.values(OrderByDirection).join(
'**, **',
)}**.
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
required: false,
schema: {
type: 'string',
},
examples: {
simple: {
value: `createdAt`,
summary: 'A simple order_by param',
},
complex: {
value: `id[${OrderByDirection.AscNullsFirst}],createdAt[${OrderByDirection.DescNullsLast}]`,
summary: 'A more complex order_by param',
},
},
};
};
export const computeDepthParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'depth',
in: 'query',
description: 'Limits the depth objects returned.',
required: false,
schema: {
type: 'integer',
enum: [1, 2],
},
};
};
export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'filter',
in: 'query',
description: `Filters objects returned.
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2,...**
Available comparators are **${Object.values(FilterComparators).join(
'**, **',
)}**.
You can create more complex filters using conjunctions **${Object.values(
Conjunctions,
).join('**, **')}**.
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
required: false,
schema: {
type: 'string',
},
examples: {
simple: {
value: 'createdAt[gte]:"2023-01-01"',
description: 'A simple filter param',
},
complex: {
value:
'or(createdAt[gte]:"2024-01-01",createdAt[lte]:"2023-01-01",not(id[is]:NULL))',
description: 'A more complex filter param',
},
},
};
};
export const computeLastCursorParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'last_cursor',
in: 'query',
description: 'Returns objects starting from a specific cursor.',
required: false,
schema: {
type: 'string',
},
};
};
export const computeIdPathParameter = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'id',
in: 'path',
description: 'Object id.',
required: true,
schema: {
type: 'string',
format: 'uuid',
},
};
};

View File

@ -0,0 +1,116 @@
import { OpenAPIV3_1 } from 'openapi-types';
import { capitalize } from 'src/utils/capitalize';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
getDeleteResponse200,
getJsonResponse,
getManyResultResponse200,
getSingleResultSuccessResponse,
} from 'src/engine/core-modules/open-api/utils/responses.utils';
import { getRequestBody } from 'src/engine/core-modules/open-api/utils/request-body.utils';
export const computeManyResultPath = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.PathItemObject => {
return {
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}**`,
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' },
],
responses: {
'200': getManyResultResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
post: {
tags: [item.namePlural],
summary: `Create One ${item.nameSingular}`,
operationId: `createOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/depth' }],
requestBody: getRequestBody(item),
responses: {
'201': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
} as OpenAPIV3_1.PathItemObject;
};
export const computeSingleResultPath = (
item: ObjectMetadataEntity,
): OpenAPIV3_1.PathItemObject => {
return {
get: {
tags: [item.namePlural],
summary: `Find One ${item.nameSingular}`,
description: `**depth** can be provided to request your **${item.nameSingular}**`,
operationId: `findOne${capitalize(item.nameSingular)}`,
parameters: [
{ $ref: '#/components/parameters/idPath' },
{ $ref: '#/components/parameters/depth' },
],
responses: {
'200': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
delete: {
tags: [item.namePlural],
summary: `Delete One ${item.nameSingular}`,
operationId: `deleteOne${capitalize(item.nameSingular)}`,
parameters: [{ $ref: '#/components/parameters/idPath' }],
responses: {
'200': getDeleteResponse200(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
put: {
tags: [item.namePlural],
summary: `Update One ${item.namePlural}`,
operationId: `UpdateOne${capitalize(item.nameSingular)}`,
parameters: [
{ $ref: '#/components/parameters/idPath' },
{ $ref: '#/components/parameters/depth' },
],
requestBody: getRequestBody(item),
responses: {
'200': getSingleResultSuccessResponse(item),
'400': { $ref: '#/components/responses/400' },
'401': { $ref: '#/components/responses/401' },
},
},
} as OpenAPIV3_1.PathItemObject;
};
export const computeOpenApiPath = (
serverUrl: string,
): OpenAPIV3_1.PathItemObject => {
return {
get: {
tags: ['General'],
summary: 'Get Open Api Schema',
operationId: 'GetOpenApiSchema',
servers: [
{
url: serverUrl,
},
],
responses: {
'200': getJsonResponse(),
},
},
} as OpenAPIV3_1.PathItemObject;
};

View File

@ -0,0 +1,18 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const getRequestBody = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
description: 'body',
required: true,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
},
},
};
};

View File

@ -0,0 +1,152 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { capitalize } from 'src/utils/capitalize';
export const getManyResultResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular' | 'namePlural'>,
) => {
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[item.namePlural]: {
type: 'array',
items: {
$ref: `#/components/schemas/${capitalize(
item.nameSingular,
)}`,
},
},
},
},
},
example: {
data: {
[item.namePlural]: [
`${capitalize(item.nameSingular)}Object`,
'...',
],
},
},
},
},
},
};
};
export const getSingleResultSuccessResponse = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[item.nameSingular]: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
},
},
},
},
},
},
};
};
export const getDeleteResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
[item.nameSingular]: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'uuid',
},
},
},
},
},
},
},
},
},
};
};
export const getJsonResponse = () => {
return {
description: 'Successful operation',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
openapi: { type: 'string' },
info: {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string' },
termsOfService: { type: 'string' },
contact: {
type: 'object',
properties: { email: { type: 'string' } },
},
license: {
type: 'object',
properties: {
name: { type: 'string' },
url: { type: 'string' },
},
},
},
},
servers: {
type: 'array',
items: {
url: { type: 'string' },
description: { type: 'string' },
},
},
components: {
type: 'object',
properties: {
schemas: { type: 'object' },
parameters: { type: 'object' },
responses: { type: 'object' },
},
},
paths: {
type: 'object',
},
tags: {
type: 'object',
},
},
},
},
},
};
};