mirror of https://github.com/nocodb/nocodb
Browse Source
* wip: swagger endpoint for all data apis in project Signed-off-by: Pranav C <pranavxc@gmail.com> * wip: swagger endpoint for all data apis in project Signed-off-by: Pranav C <pranavxc@gmail.com> * wip(swagger): add description, sample and improve Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: add nested column params Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: add auth header in swagger.json Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: populate enum type for columnnames Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: swagger apis for views Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: add swagger and redoc url in GUI Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: limit fields in model api based on fields query parameter Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: project settings navdrawer background color correction re #1793 Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: update swagger server url and update swagger icon Signed-off-by: Pranav C <pranavxc@gmail.com>pull/1840/head
Pranav C
2 years ago
committed by
GitHub
25 changed files with 44568 additions and 358 deletions
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
||||
import Noco from '../../../../Noco'; |
||||
import Model from '../../../../../noco-models/Model'; |
||||
import Project from '../../../../../noco-models/Project'; |
||||
import { getModelPaths, getViewPaths } from './templates/paths'; |
||||
import { SwaggerColumn } from './getSwaggerColumnMetas'; |
||||
import { 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: 'noco', |
||||
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: 'noco', |
||||
columns: swaggerColumns, |
||||
projectName: project.title |
||||
}) |
||||
); |
||||
} |
||||
|
||||
return swaggerPaths; |
||||
} |
@ -0,0 +1,46 @@
|
||||
import Noco from '../../../../Noco'; |
||||
import Model from '../../../../../noco-models/Model'; |
||||
import Project from '../../../../../noco-models/Project'; |
||||
import { getModelSchemas, getViewSchemas } from './templates/schemas'; |
||||
import { SwaggerColumn } from './getSwaggerColumnMetas'; |
||||
import { 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: 'noco', |
||||
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: 'noco', |
||||
columns: swaggerColumns, |
||||
projectName: project.title |
||||
}) |
||||
); |
||||
} |
||||
|
||||
return swaggerSchemas; |
||||
} |
@ -0,0 +1,59 @@
|
||||
import { UITypes } from 'nocodb-sdk'; |
||||
import LinkToAnotherRecordColumn from '../../../../../noco-models/LinkToAnotherRecordColumn'; |
||||
import SwaggerTypes from '../../../../../sqlMgr/code/routers/xc-ts/SwaggerTypes'; |
||||
import Column from '../../../../../noco-models/Column'; |
||||
import Noco from '../../../../Noco'; |
||||
import Project from '../../../../../noco-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 |
||||
); |
||||
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; |
||||
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; |
||||
} |
@ -0,0 +1,66 @@
|
||||
import Noco from '../../../../Noco'; |
||||
import Model from '../../../../../noco-models/Model'; |
||||
import swaggerBase from './swagger-base.json'; |
||||
import getPaths from './getPaths'; |
||||
import getSchemas from './getSchemas'; |
||||
import Project from '../../../../../noco-models/Project'; |
||||
import getSwaggerColumnMetas from './getSwaggerColumnMetas'; |
||||
import { ViewTypes } from 'nocodb-sdk'; |
||||
import GridViewColumn from '../../../../../noco-models/GridViewColumn'; |
||||
import View from '../../../../../noco-models/View'; |
||||
|
||||
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>; |
||||
} |
@ -0,0 +1,61 @@
|
||||
{ |
||||
"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" |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"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": [] |
||||
} |
||||
] |
||||
} |
@ -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' |
||||
} |
||||
}; |
@ -0,0 +1,192 @@
|
||||
import { SwaggerColumn } from '../getSwaggerColumnMetas'; |
||||
import { RelationTypes, UITypes } from 'nocodb-sdk'; |
||||
import LinkToAnotherRecordColumn from '../../../../../../noco-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 and default value is `25`.', |
||||
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 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: 'enum', |
||||
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) |
||||
]; |
||||
} |
||||
} |
||||
|
||||
return paramsArr; |
||||
}, Promise.resolve([])); |
||||
}; |
@ -0,0 +1,611 @@
|
||||
import { ModelTypes, UITypes } from 'nocodb-sdk'; |
||||
import { |
||||
columnNameParam, |
||||
csvExportOffsetParam, |
||||
exportTypeParam, |
||||
fieldsParam, |
||||
getNestedParams, |
||||
limitParam, |
||||
offsetParam, |
||||
referencedRowIdParam, |
||||
relationTypeParam, |
||||
rowIdParam, |
||||
sortParam, |
||||
whereParam |
||||
} from './params'; |
||||
import { csvExportResponseHeader } from './headers'; |
||||
import { 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: '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, |
||||
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: {} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
...(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], |
||||
requestBody: { |
||||
content: { |
||||
'application/json': { |
||||
schema: {} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
|
||||
...(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, 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, 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], |
||||
wrapped: true, |
||||
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: '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 )`], |
||||
wrapped: true, |
||||
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 |
||||
); |
||||
} |
@ -0,0 +1,85 @@
|
||||
import { 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 |
||||
}) |
||||
}), |
||||
{} |
||||
) || {}) |
||||
} |
||||
} |
||||
}); |
@ -0,0 +1,24 @@
|
||||
export default `<!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="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> |
||||
|
||||
<!-- |
||||
Redoc doesn't change outer page styles |
||||
--> |
||||
<style> |
||||
body { |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<redoc spec-url='./swagger.json'></redoc> |
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script> |
||||
</body> |
||||
</html>`;
|
@ -0,0 +1,73 @@
|
||||
// @ts-ignore
|
||||
import catchError from '../../helpers/catchError'; |
||||
import { Request, Router } from 'express'; |
||||
import Model from '../../../../noco-models/Model'; |
||||
import getSwaggerJSON from './helpers/getSwaggerJSON'; |
||||
import Project from '../../../../noco-models/Project'; |
||||
import swaggerHtml from './swaggerHtml'; |
||||
import redocHtml from './redocHtml'; |
||||
const Converter = require('openapi-to-postmanv2'); |
||||
|
||||
async function swaggerJson(req, res) { |
||||
const project = await Project.get(req.params.projectId); |
||||
const models = await Model.list({ |
||||
project_id: req.params.project_id, |
||||
base_id: null |
||||
}); |
||||
|
||||
const swagger = await getSwaggerJSON(project, models); |
||||
res.json(swagger); |
||||
} |
||||
|
||||
async function postmanJson(req: Request, res) { |
||||
const project = await Project.get(req.params.projectId); |
||||
const models = await Model.list({ |
||||
project_id: req.params.project_id, |
||||
base_id: null |
||||
}); |
||||
|
||||
const swagger = await getSwaggerJSON(project, models); |
||||
|
||||
swagger.servers = [ |
||||
{ |
||||
url: (req as any).ncSiteUrl |
||||
} |
||||
]; |
||||
|
||||
Converter.convert( |
||||
{ type: 'json', data: swagger }, |
||||
{}, |
||||
(err, conversionResult) => { |
||||
if (err) { |
||||
res.status(400).json({ msg: err }); |
||||
} |
||||
|
||||
if (!conversionResult.result) { |
||||
res |
||||
.status(400) |
||||
.json({ msg: 'Could not convert : ' + conversionResult.reason }); |
||||
} else { |
||||
res.json(conversionResult.output[0].data); |
||||
} |
||||
} |
||||
); |
||||
} |
||||
const router = Router({ mergeParams: true }); |
||||
|
||||
// todo: auth
|
||||
router.get( |
||||
'/api/v1/db/meta/projects/:projectId/swagger.json', |
||||
catchError(swaggerJson) |
||||
); |
||||
router.get( |
||||
'/api/v1/db/meta/projects/:projectId/postman.json', |
||||
catchError(postmanJson) |
||||
); |
||||
router.get('/api/v1/db/meta/projects/:projectId/swagger', (_req, res) => |
||||
res.send(swaggerHtml) |
||||
); |
||||
router.get('/api/v1/db/meta/projects/:projectId/redoc', (_req, res) => |
||||
res.send(redocHtml) |
||||
); |
||||
|
||||
export default router; |
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue