Browse Source

feat: swagger/redoc apis and page

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 1 year ago
parent
commit
50ea5e1bb7
  1. 3
      packages/nocodb-nest/src/app.module.ts
  2. 20
      packages/nocodb-nest/src/modules/api-docs/api-docs.controller.spec.ts
  3. 28
      packages/nocodb-nest/src/modules/api-docs/api-docs.controller.ts
  4. 9
      packages/nocodb-nest/src/modules/api-docs/api-docs.module.ts
  5. 18
      packages/nocodb-nest/src/modules/api-docs/api-docs.service.spec.ts
  6. 37
      packages/nocodb-nest/src/modules/api-docs/api-docs.service.ts
  7. 48
      packages/nocodb-nest/src/modules/api-docs/swagger/getPaths.ts
  8. 46
      packages/nocodb-nest/src/modules/api-docs/swagger/getSchemas.ts
  9. 68
      packages/nocodb-nest/src/modules/api-docs/swagger/getSwaggerColumnMetas.ts
  10. 66
      packages/nocodb-nest/src/modules/api-docs/swagger/getSwaggerJSON.ts
  11. 96
      packages/nocodb-nest/src/modules/api-docs/swagger/swagger-base.json
  12. 10
      packages/nocodb-nest/src/modules/api-docs/swagger/templates/headers.ts
  13. 217
      packages/nocodb-nest/src/modules/api-docs/swagger/templates/params.ts
  14. 676
      packages/nocodb-nest/src/modules/api-docs/swagger/templates/paths.ts
  15. 85
      packages/nocodb-nest/src/modules/api-docs/swagger/templates/schemas.ts
  16. 93
      packages/nocodb-nest/src/modules/api-docs/template/redocHtml.ts
  17. 87
      packages/nocodb-nest/src/modules/api-docs/template/swaggerHtml.ts

3
packages/nocodb-nest/src/app.module.ts

@ -37,9 +37,10 @@ import { MetaDiffsModule } from './modules/meta-diffs/meta-diffs.module';
import { AuditsModule } from './modules/audits/audits.module';
import { DatasModule } from './modules/datas/datas.module';
import { DataAliasModule } from './modules/data-alias/data-alias.module';
import { ApiDocsModule } from './modules/api-docs/api-docs.module';
@Module({
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule, ProjectUsersModule, ModelVisibilitiesModule, HookFiltersModule, ApiTokensModule, AttachmentsModule, OrgLcenseModule, OrgTokensModule, OrgUsersModule, MetaDiffsModule, AuditsModule, DatasModule, DataAliasModule],
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule, ProjectUsersModule, ModelVisibilitiesModule, HookFiltersModule, ApiTokensModule, AttachmentsModule, OrgLcenseModule, OrgTokensModule, OrgUsersModule, MetaDiffsModule, AuditsModule, DatasModule, DataAliasModule, ApiDocsModule],
controllers: [],
providers: [Connection, MetaService, JwtStrategy, ExtractProjectIdMiddleware],
exports: [Connection, MetaService],

20
packages/nocodb-nest/src/modules/api-docs/api-docs.controller.spec.ts

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApiDocsController } from './api-docs.controller';
import { ApiDocsService } from './api-docs.service';
describe('ApiDocsController', () => {
let controller: ApiDocsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ApiDocsController],
providers: [ApiDocsService],
}).compile();
controller = module.get<ApiDocsController>(ApiDocsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

28
packages/nocodb-nest/src/modules/api-docs/api-docs.controller.ts

@ -0,0 +1,28 @@
import { Controller, Get, Param, Request, Response } from '@nestjs/common';
import { ApiDocsService } from './api-docs.service';
import getSwaggerHtml from './template/swaggerHtml';
import getRedocHtml from './template/redocHtml';
@Controller('api-docs')
export class ApiDocsController {
constructor(private readonly apiDocsService: ApiDocsService) {}
async swaggerJson(@Param('projectId') projectId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJson({
projectId: projectId,
siteUrl: req.ncSiteUrl,
});
return swagger;
}
@Get('/api/v1/db/meta/projects/:projectId/swagger')
swaggerHtml(@Param('projectId') projectId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@Get('/api/v1/db/meta/projects/:projectId/redoc')
redocHtml(@Param('projectId') projectId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
}

9
packages/nocodb-nest/src/modules/api-docs/api-docs.module.ts

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ApiDocsService } from './api-docs.service';
import { ApiDocsController } from './api-docs.controller';
@Module({
controllers: [ApiDocsController],
providers: [ApiDocsService]
})
export class ApiDocsModule {}

18
packages/nocodb-nest/src/modules/api-docs/api-docs.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApiDocsService } from './api-docs.service';
describe('ApiDocsService', () => {
let service: ApiDocsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ApiDocsService],
}).compile();
service = module.get<ApiDocsService>(ApiDocsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

37
packages/nocodb-nest/src/modules/api-docs/api-docs.service.ts

@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { NcError } from '../../helpers/catchError';
import { Model, Project } from '../../models';
import getSwaggerJSON from './swagger/getSwaggerJSON';
@Injectable()
export class ApiDocsService {
async swaggerJson(param: { projectId: string; siteUrl: string }) {
const project = await Project.get(param.projectId);
if (!project) NcError.notFound();
const models = await Model.list({
project_id: param.projectId,
base_id: null,
});
const swagger = await getSwaggerJSON(project, models);
swagger.servers = [
{
url: param.siteUrl,
},
{
url: '{customUrl}',
variables: {
customUrl: {
default: param.siteUrl,
description: 'Provide custom nocodb app base url',
},
},
},
] as any;
return swagger;
}
}

48
packages/nocodb-nest/src/modules/api-docs/swagger/getPaths.ts

@ -0,0 +1,48 @@
import Noco from '../../Noco';
import { getModelPaths, getViewPaths } from './templates/paths';
import type Model from '../../models/Model';
import type Project from '../../models/Project';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSON';
export default async function getPaths(
{
project,
model,
columns,
views,
}: {
project: Project;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta
) {
const swaggerPaths = await getModelPaths({
tableName: model.title,
type: model.type,
orgs: 'v1',
columns,
projectName: project.title,
});
for (const { view, columns: viewColumns } of views) {
const swaggerColumns = columns.filter(
(c) => viewColumns.find((vc) => vc.fk_column_id === c.column.id)?.show
);
Object.assign(
swaggerPaths,
await getViewPaths({
tableName: model.title,
viewName: view.title,
type: model.type,
orgs: 'v1',
columns: swaggerColumns,
projectName: project.title,
})
);
}
return swaggerPaths;
}

46
packages/nocodb-nest/src/modules/api-docs/swagger/getSchemas.ts

@ -0,0 +1,46 @@
import Noco from '../../Noco';
import { getModelSchemas, getViewSchemas } from './templates/schemas';
import type Model from '../../models/Model';
import type Project from '../../models/Project';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSON';
export default async function getSchemas(
{
project,
model,
columns,
views,
}: {
project: Project;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta
) {
const swaggerSchemas = getModelSchemas({
tableName: model.title,
orgs: 'v1',
projectName: project.title,
columns,
});
for (const { view, columns: viewColumns } of views) {
const swaggerColumns = columns.filter(
(c) => viewColumns.find((vc) => vc.fk_column_id === c.column.id)?.show
);
Object.assign(
swaggerSchemas,
getViewSchemas({
tableName: model.title,
viewName: view.title,
orgs: 'v1',
columns: swaggerColumns,
projectName: project.title,
})
);
}
return swaggerSchemas;
}

68
packages/nocodb-nest/src/modules/api-docs/swagger/getSwaggerColumnMetas.ts

@ -0,0 +1,68 @@
import { UITypes } from 'nocodb-sdk';
import SwaggerTypes from '../../db/sql-mgr/code/routers/xc-ts/SwaggerTypes';
import Noco from '../../Noco';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type Column from '../../models/Column';
import type Project from '../../models/Project';
export default async (
columns: Column[],
project: Project,
ncMeta = Noco.ncMeta
): Promise<SwaggerColumn[]> => {
const dbType = await project.getBases().then((b) => b?.[0]?.type);
return Promise.all(
columns.map(async (c) => {
const field: SwaggerColumn = {
title: c.title,
type: 'object',
virtual: true,
column: c,
};
switch (c.uidt) {
case UITypes.LinkToAnotherRecord:
{
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(
ncMeta
);
if (colOpt) {
const relTable = await colOpt.getRelatedTable(ncMeta);
field.type = undefined;
field.$ref = `#/components/schemas/${relTable.title}Request`;
}
}
break;
case UITypes.Formula:
case UITypes.Lookup:
field.type = 'object';
break;
case UITypes.Rollup:
field.type = 'number';
break;
case UITypes.Attachment:
field.type = 'array';
field.items = {
$ref: `#/components/schemas/Attachment`,
};
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);
break;
}
return field;
})
);
};
export interface SwaggerColumn {
type: any;
title: string;
description?: string;
virtual?: boolean;
$ref?: any;
column: Column;
items?: any;
}

66
packages/nocodb-nest/src/modules/api-docs/swagger/getSwaggerJSON.ts

@ -0,0 +1,66 @@
import { ViewTypes } from 'nocodb-sdk';
import Noco from '../../Noco';
import swaggerBase from './swagger-base.json';
import getPaths from './getPaths';
import getSchemas from './getSchemas';
import getSwaggerColumnMetas from './getSwaggerColumnMetas';
import type GalleryViewColumn from '../../models/GalleryViewColumn';
import type FormViewColumn from '../../models/FormViewColumn';
import type { Model, Project, View } from '../../models';
import type GridViewColumn from '../../models/GridViewColumn';
export default async function getSwaggerJSON(
project: Project,
models: Model[],
ncMeta = Noco.ncMeta
) {
// base swagger object
const swaggerObj = {
...swaggerBase,
paths: {},
components: {
...swaggerBase.components,
schemas: { ...swaggerBase.components.schemas },
},
};
// iterate and populate swagger schema and path for models and views
for (const model of models) {
let paths = {};
const columns = await getSwaggerColumnMetas(
await model.getColumns(ncMeta),
project,
ncMeta
);
const views: SwaggerView[] = [];
for (const view of (await model.getViews(false, ncMeta)) || []) {
if (view.type !== ViewTypes.GRID) continue;
views.push({
view,
columns: await view.getColumns(ncMeta),
});
}
// skip mm tables
if (!model.mm)
paths = await getPaths({ project, model, columns, views }, ncMeta);
const schemas = await getSchemas(
{ project, model, columns, views },
ncMeta
);
Object.assign(swaggerObj.paths, paths);
Object.assign(swaggerObj.components.schemas, schemas);
}
return swaggerObj;
}
export interface SwaggerView {
view: View;
columns: Array<GridViewColumn | GalleryViewColumn | FormViewColumn>;
}

96
packages/nocodb-nest/src/modules/api-docs/swagger/swagger-base.json

@ -0,0 +1,96 @@
{
"openapi": "3.0.0",
"info": {
"title": "nocodb",
"version": "1.0"
},
"servers": [
{
"url": "http://localhost:8080"
}
],
"paths": {
},
"components": {
"schemas": {
"Paginated": {
"title": "Paginated",
"type": "object",
"properties": {
"pageSize": {
"type": "integer"
},
"totalRows": {
"type": "integer"
},
"isFirstPage": {
"type": "boolean"
},
"isLastPage": {
"type": "boolean"
},
"page": {
"type": "number"
}
}
},
"Attachment": {
"title": "Attachment",
"type": "object",
"properties": {
"mimetype": {
"type": "string"
},
"size": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"icon": {
"type": "string"
}
}
},
"Groupby": {
"title": "Groupby",
"type": "object",
"properties": {
"count": {
"type": "number",
"description": "count"
},
"column_name": {
"type": "string",
"description": "the value of the given column"
}
}
}
},
"securitySchemes": {
"xcAuth": {
"type": "apiKey",
"in": "header",
"name": "xc-auth",
"description": "JWT access token"
},
"xcToken": {
"type": "apiKey",
"in": "header",
"name": "xc-token",
"description": "API token"
}
}
},
"security": [
{
"xcAuth": []
},
{
"xcToken": []
}
]
}

10
packages/nocodb-nest/src/modules/api-docs/swagger/templates/headers.ts

@ -0,0 +1,10 @@
export const csvExportResponseHeader = {
'nc-export-offset': {
schema: {
type: 'integer',
},
description:
'Offset of next set of data which will be helpful if there is large amount of data. It will returns `-1` if all set of data exported.',
example: '1000',
},
};

217
packages/nocodb-nest/src/modules/api-docs/swagger/templates/params.ts

@ -0,0 +1,217 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
import type LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
export const rowIdParam = {
schema: {
type: 'string',
},
name: 'rowId',
in: 'path',
required: true,
example: 1,
description:
'Primary key of the record you want to read. If the table have composite primary key then combine them by using `___` and pass it as primary key.',
};
export const relationTypeParam = {
schema: {
type: 'string',
enum: ['mm', 'hm'],
},
name: 'relationType',
in: 'path',
required: true,
};
export const fieldsParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'fields',
description:
'Array of field names or comma separated filed names to include in the response objects. In array syntax pass it like `fields[]=field1&fields[]=field2` or alternately `fields=field1,field2`.',
};
export const sortParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'sort',
description:
'Comma separated field names to sort rows, rows will sort in ascending order based on provided columns. To sort in descending order provide `-` prefix along with column name, like `-field`. Example : `sort=field1,-field2`',
};
export const whereParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'where',
description:
'This can be used for filtering rows, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : `where=(field1,eq,value)`',
};
export const limitParam = {
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: 'limit',
description:
'The `limit` parameter used for pagination, the response collection size depends on limit value with default value `25` and maximum value `1000`, which can be overridden by environment variables `DB_QUERY_LIMIT_DEFAULT` and `DB_QUERY_LIMIT_MAX` respectively.',
example: 25,
};
export const offsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'The `offset` parameter used for pagination, the value helps to select collection from a certain index.',
example: 0,
};
export const shuffleParam = {
schema: {
type: 'number',
minimum: 0,
maximum: 1,
},
in: 'query',
name: 'shuffle',
description:
'The `shuffle` parameter used for pagination, the response will be shuffled if it is set to 1.',
example: 0,
};
export const columnNameQueryParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'column_name',
description:
'Column name of the column you want to group by, eg. `column_name=column1`',
};
export const columnNameParam = (columns: SwaggerColumn[]) => {
const columnNames = [];
for (const { column } of columns) {
if (column.uidt !== UITypes.LinkToAnotherRecord || column.system) continue;
columnNames.push(column.title);
}
return {
schema: {
type: 'string',
enum: columnNames,
},
name: 'columnName',
in: 'path',
required: true,
};
};
export const referencedRowIdParam = {
schema: {
type: 'string',
},
name: 'refRowId',
in: 'path',
required: true,
};
export const exportTypeParam = {
schema: {
type: 'string',
enum: ['csv', 'excel'],
},
name: 'type',
in: 'path',
required: true,
};
export const csvExportOffsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'Helps to start export from a certain index. You can get the next set of data offset from previous response header named `nc-export-offset`.',
example: 0,
};
export const nestedWhereParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][where]`,
description: `This can be used for filtering rows in nested column \`${colName}\`, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : \`nested[${colName}][where]=(field1,eq,value)\``,
});
export const nestedFieldParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][fields]`,
description: `Array of field names or comma separated filed names to include in the in nested column \`${colName}\` result. In array syntax pass it like \`fields[]=field1&fields[]=field2.\`. Example : \`nested[${colName}][fields]=field1,field2\``,
});
export const nestedSortParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][sort]`,
description: `Comma separated field names to sort rows in nested column \`${colName}\` rows, it will sort in ascending order based on provided columns. To sort in descending order provide \`-\` prefix along with column name, like \`-field\`. Example : \`nested[${colName}][sort]=field1,-field2\``,
});
export const nestedLimitParam = (colName) => ({
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: `nested[${colName}][limit]`,
description: `The \`limit\` parameter used for pagination of nested \`${colName}\` rows, the response collection size depends on limit value and default value is \`25\`.`,
example: '25',
});
export const nestedOffsetParam = (colName) => ({
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: `nested[${colName}][offset]`,
description: `The \`offset\` parameter used for pagination of nested \`${colName}\` rows, the value helps to select collection from a certain index.`,
example: 0,
});
export const getNestedParams = async (
columns: SwaggerColumn[]
): Promise<any[]> => {
return await columns.reduce(async (paramsArr, { column }) => {
if (column.uidt === UITypes.LinkToAnotherRecord) {
const colOpt = await column.getColOptions<LinkToAnotherRecordColumn>();
if (colOpt.type !== RelationTypes.BELONGS_TO) {
return [
...(await paramsArr),
nestedWhereParam(column.title),
nestedOffsetParam(column.title),
nestedLimitParam(column.title),
nestedFieldParam(column.title),
nestedSortParam(column.title),
];
} else {
return [...(await paramsArr), nestedFieldParam(column.title)];
}
}
return paramsArr;
}, Promise.resolve([]));
};

676
packages/nocodb-nest/src/modules/api-docs/swagger/templates/paths.ts

@ -0,0 +1,676 @@
import { ModelTypes, UITypes } from 'nocodb-sdk';
import {
columnNameParam,
columnNameQueryParam,
csvExportOffsetParam,
exportTypeParam,
fieldsParam,
getNestedParams,
limitParam,
offsetParam,
referencedRowIdParam,
relationTypeParam,
rowIdParam,
shuffleParam,
sortParam,
whereParam,
} from './params';
import { csvExportResponseHeader } from './headers';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
export const getModelPaths = async (ctx: {
tableName: string;
orgs: string;
type: ModelTypes;
columns: SwaggerColumn[];
projectName: string;
}): Promise<{ [path: string]: any }> => ({
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}`]: {
get: {
summary: `${ctx.tableName} list`,
operationId: `${ctx.tableName.toLowerCase()}-db-table-row-list`,
description: `List of all rows from ${ctx.tableName} ${ctx.type} and response data fields can be filtered based on query params.`,
tags: [ctx.tableName],
parameters: [
fieldsParam,
sortParam,
whereParam,
limitParam,
shuffleParam,
offsetParam,
...(await getNestedParams(ctx.columns)),
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(`${ctx.tableName}Response`),
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.tableName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName.toLowerCase()}-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
},
},
},
}
: {}),
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}`]: {
parameters: [rowIdParam],
...(ctx.type === ModelTypes.TABLE
? {
get: {
parameters: [fieldsParam],
summary: `${ctx.tableName} read`,
description:
'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName.toLowerCase()}-read`,
responses: {
'201': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
},
tags: [ctx.tableName],
},
patch: {
summary: `${ctx.tableName} update`,
operationId: `${ctx.tableName.toLowerCase()}-update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {},
},
},
},
},
delete: {
summary: `${ctx.tableName} delete`,
operationId: `${ctx.tableName.toLowerCase()}-delete`,
responses: {
'200': {
description: 'OK',
},
},
tags: [ctx.tableName],
description:
'Delete a row by using the **primary key** column value.',
},
}
: {}),
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/count`]: {
get: {
summary: `${ctx.tableName} count`,
operationId: `${ctx.tableName.toLowerCase()}-count`,
description: 'Get rows count of a table by applying optional filters.',
tags: [ctx.tableName],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
},
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/find-one`]:
{
get: {
summary: `${ctx.tableName} find-one`,
operationId: `${ctx.tableName.toLowerCase()}-db-table-row-find-one`,
description: `Find first record matching the conditions.`,
tags: [ctx.tableName],
parameters: [fieldsParam, whereParam, sortParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
},
},
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/groupby`]: {
get: {
summary: `${ctx.tableName} groupby`,
operationId: `${ctx.tableName.toLowerCase()}-groupby`,
description: 'Group by a column.',
tags: [ctx.tableName],
parameters: [
columnNameQueryParam,
sortParam,
whereParam,
limitParam,
offsetParam,
shuffleParam,
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
list: {
type: 'array',
items: {
$ref: `#/components/schemas/Groupby`,
},
},
PageInfo: {
$ref: `#/components/schemas/Paginated`,
},
},
},
},
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
[`/api/v1/db/data/bulk/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}`]:
{
post: {
summary: `${ctx.tableName} bulk insert`,
description:
"To insert large amount of data in a single api call you can use this api. It's similar to insert method but here you can pass array of objects to insert into table. Array object will be key value paired column name and value.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {},
},
},
},
},
patch: {
summary: `${ctx.tableName} bulk update`,
description:
"To update multiple records using it's primary key you can use this api. Bulk updated api accepts array object in which each object should contain it's primary columns value mapped to corresponding alias. In addition to primary key you can include the fields which you want to update",
operationId: `${ctx.tableName.toLowerCase()}-bulk-update`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {},
},
},
},
},
delete: {
summary: `${ctx.tableName} bulk delete by IDs`,
description:
"To delete multiple records using it's primary key you can use this api. Bulk delete api accepts array object in which each object should contain it's primary columns value mapped to corresponding alias.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-delete`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {},
},
},
},
},
},
[`/api/v1/db/data/bulk/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/all`]:
{
parameters: [whereParam],
patch: {
summary: `${ctx.tableName} Bulk update with conditions`,
description:
"This api helps you update multiple table rows in a single api call. You don't have to pass the record id instead you can filter records and apply the changes to filtered records. Payload is similar as normal update in which you can pass any partial row data to be updated.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-update-all`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {},
},
},
},
},
delete: {
summary: 'Bulk delete with conditions',
description:
"This api helps you delete multiple table rows in a single api call. You don't have to pass the record id instead you can filter records and delete filtered records.",
operationId: `${ctx.tableName.toLowerCase()}-bulk-delete-all`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
},
},
...(isRelationExist(ctx.columns)
? {
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}`]:
{
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns),
],
get: {
summary: 'Relation row list',
operationId: `${ctx.tableName.toLowerCase()}-nested-list`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam],
},
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}/{refRowId}`]:
{
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns),
referencedRowIdParam,
],
post: {
summary: 'Relation row add',
operationId: `${ctx.tableName.toLowerCase()}-nested-add`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
parameters: [limitParam, shuffleParam, offsetParam],
description: '',
},
delete: {
summary: 'Relation row remove',
operationId: `${ctx.tableName.toLowerCase()}-nested-remove`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
},
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/{rowId}/{relationType}/{columnName}/exclude`]:
{
parameters: [
rowIdParam,
relationTypeParam,
columnNameParam(ctx.columns),
],
get: {
summary:
'Referenced tables rows excluding current records children/parent',
operationId: `${ctx.tableName.toLowerCase()}-nested-children-excluded-list`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [ctx.tableName],
parameters: [limitParam, shuffleParam, offsetParam],
},
},
}
: {}),
}
: {}),
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/export/{type}`]:
{
parameters: [exportTypeParam],
get: {
summary: 'Rows export',
operationId: `${ctx.tableName.toLowerCase()}-csv-export`,
description:
'Export all the records from a table.Currently we are only supports `csv` export.',
tags: [ctx.tableName],
responses: {
'200': {
description: 'OK',
content: {
'application/octet-stream': {
schema: {},
},
},
headers: csvExportResponseHeader,
},
},
parameters: [csvExportOffsetParam],
},
},
});
export const getViewPaths = async (ctx: {
tableName: string;
viewName: string;
type: ModelTypes;
orgs: string;
projectName: string;
columns: SwaggerColumn[];
}): Promise<any> => ({
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}`]:
{
get: {
summary: `${ctx.viewName} list`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-list`,
description: `List of all rows from ${ctx.viewName} grid view and data of fields can be filtered based on query params. Data and fields in a grid view will be filtered and sorted by default based on the applied options in Dashboard.`,
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
parameters: [
fieldsParam,
sortParam,
whereParam,
...(await getNestedParams(ctx.columns)),
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(
`${ctx.tableName}${ctx.viewName}GridResponse`
),
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.viewName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridRequest`,
},
},
},
},
},
}
: {}),
},
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/count`]:
{
get: {
summary: `${ctx.viewName} count`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-count`,
description: '',
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
count: { type: 'number' },
},
},
},
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/{rowId}`]:
{
parameters: [rowIdParam],
get: {
summary: `${ctx.viewName} read`,
description:
'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-read`,
responses: {
'200': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridResponse`,
},
},
},
},
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
},
patch: {
summary: `${ctx.viewName} update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
operationId: `${ctx.tableName}-${ctx.viewName}-row-update`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
requestBody: {
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}${ctx.viewName}GridRequest`,
},
},
},
},
},
delete: {
summary: `${ctx.viewName} delete`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-delete`,
responses: {
'200': {
description: 'OK',
},
},
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
description:
'Delete a row by using the **primary key** column value.',
},
},
}
: {}),
[`/api/v1/db/data/${ctx.orgs}/${ctx.projectName}/${ctx.tableName}/views/${ctx.viewName}/export/{type}`]:
{
parameters: [exportTypeParam],
get: {
summary: `${ctx.viewName} export`,
operationId: `${ctx.tableName}-${ctx.viewName}-row-export`,
description:
'Export all the records from a table view. Currently we are only supports `csv` export.',
tags: [`${ctx.viewName} ( ${ctx.tableName} grid )`],
responses: {
'200': {
description: 'OK',
content: {
'application/octet-stream': {
schema: {},
},
},
headers: csvExportResponseHeader,
},
},
parameters: [],
},
},
});
function getPaginatedResponseType(type: string) {
return {
type: 'object',
properties: {
list: {
type: 'array',
items: {
$ref: `#/components/schemas/${type}`,
},
},
PageInfo: {
$ref: `#/components/schemas/Paginated`,
},
},
};
}
function isRelationExist(columns: SwaggerColumn[]) {
return columns.some(
(c) => c.column.uidt === UITypes.LinkToAnotherRecord && !c.column.system
);
}

85
packages/nocodb-nest/src/modules/api-docs/swagger/templates/schemas.ts

@ -0,0 +1,85 @@
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
export const getModelSchemas = (ctx: {
tableName: string;
orgs: string;
projectName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}Response`]: {
title: `${ctx.tableName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
[title]: fieldProps,
}),
{}
) || {}),
},
},
[`${ctx.tableName}Request`]: {
title: `${ctx.tableName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual
? {}
: {
[title]: fieldProps,
}),
}),
{}
) || {}),
},
},
});
export const getViewSchemas = (ctx: {
tableName: string;
viewName: string;
orgs: string;
projectName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}${ctx.viewName}GridResponse`]: {
title: `${ctx.tableName} : ${ctx.viewName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
[title]: fieldProps,
}),
{}
) || {}),
},
},
[`${ctx.tableName}${ctx.viewName}GridRequest`]: {
title: `${ctx.tableName} : ${ctx.viewName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual
? {}
: {
[title]: fieldProps,
}),
}),
{}
) || {}),
},
},
});

93
packages/nocodb-nest/src/modules/api-docs/template/redocHtml.ts vendored

@ -0,0 +1,93 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB API Documentation</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="${ncSiteUrl}/css/fonts.montserrat.css" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="redoc"></div>
<script src="${ncSiteUrl}/js/redoc.standalone.min.js"></script>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
const xhttp = new XMLHttpRequest();
xhttp.open("GET", "./swagger.json");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xhttp.onload = function () {
const swaggerJson = this.responseText;
const swagger = JSON.parse(swaggerJson);
Redoc.init(swagger, {
scrollYOffset: 50
}, document.getElementById('redoc'))
};
xhttp.send();
</script>
<script>
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>`;

87
packages/nocodb-nest/src/modules/api-docs/template/swaggerHtml.ts vendored

@ -0,0 +1,87 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB : API Docs</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="${ncSiteUrl}/favicon.ico" />
<link rel="stylesheet" href="${ncSiteUrl}/css/swagger-ui-bundle.4.5.2.min.css"/>
<script src="${ncSiteUrl}/js/swagger-ui-bundle.4.5.2.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
var xmlhttp = new XMLHttpRequest(); // new HttpRequest instance
xmlhttp.open("GET", "./swagger.json");
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xmlhttp.onload = function () {
const ui = SwaggerUIBundle({
// url: ,
spec: JSON.parse(xmlhttp.responseText),
dom_id: '#app',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
}
xmlhttp.send();
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px');
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>
`;
Loading…
Cancel
Save