mirror of https://github.com/nocodb/nocodb
աɨռɢӄաօռɢ
1 year ago
97 changed files with 8882 additions and 5424 deletions
@ -0,0 +1,28 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { ColumnType } from 'nocodb-sdk' |
||||||
|
import { isVirtualCol } from 'nocodb-sdk' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
column: ColumnType |
||||||
|
modelValue: any |
||||||
|
}>() |
||||||
|
|
||||||
|
provide(ReadonlyInj, true) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="pointer-events-none"> |
||||||
|
<LazySmartsheetRow :row="{ row: { [column.title]: modelValue }, rowMeta: {} }"> |
||||||
|
<LazySmartsheetVirtualCell v-if="isVirtualCol(column)" :model-value="modelValue" class="!text-gray-600" :column="column" /> |
||||||
|
|
||||||
|
<LazySmartsheetCell |
||||||
|
v-else |
||||||
|
:model-value="modelValue" |
||||||
|
class="!text-gray-600" |
||||||
|
:column="column" |
||||||
|
:edit-enabled="false" |
||||||
|
:read-only="true" |
||||||
|
/> |
||||||
|
</LazySmartsheetRow> |
||||||
|
</div> |
||||||
|
</template> |
File diff suppressed because it is too large
Load Diff
@ -1,163 +0,0 @@ |
|||||||
import { RelationTypes, UITypes } from 'nocodb-sdk'; |
|
||||||
import type LookupColumn from '../models/LookupColumn'; |
|
||||||
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
|
||||||
import type { |
|
||||||
Column, |
|
||||||
FormulaColumn, |
|
||||||
LinkToAnotherRecordColumn, |
|
||||||
Model, |
|
||||||
RollupColumn, |
|
||||||
} from '~/models'; |
|
||||||
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2'; |
|
||||||
import genRollupSelectv2 from '~/db/genRollupSelectv2'; |
|
||||||
import { NcError } from '~/helpers/catchError'; |
|
||||||
|
|
||||||
export default async function generateBTLookupSelectQuery({ |
|
||||||
column, |
|
||||||
baseModelSqlv2, |
|
||||||
alias, |
|
||||||
model, |
|
||||||
}: { |
|
||||||
column: Column; |
|
||||||
baseModelSqlv2: BaseModelSqlv2; |
|
||||||
alias: string; |
|
||||||
model: Model; |
|
||||||
}): Promise<any> { |
|
||||||
const knex = baseModelSqlv2.dbDriver; |
|
||||||
|
|
||||||
const rootAlias = alias; |
|
||||||
|
|
||||||
{ |
|
||||||
let aliasCount = 0, |
|
||||||
selectQb; |
|
||||||
const alias = `__nc_lk_${aliasCount++}`; |
|
||||||
const lookup = await column.getColOptions<LookupColumn>(); |
|
||||||
{ |
|
||||||
const relationCol = await lookup.getRelationColumn(); |
|
||||||
const relation = |
|
||||||
await relationCol.getColOptions<LinkToAnotherRecordColumn>(); |
|
||||||
|
|
||||||
// if not belongs to then throw error as we don't support
|
|
||||||
if (relation.type !== RelationTypes.BELONGS_TO) |
|
||||||
NcError.badRequest('HasMany/ManyToMany lookup is not supported'); |
|
||||||
|
|
||||||
const childColumn = await relation.getChildColumn(); |
|
||||||
const parentColumn = await relation.getParentColumn(); |
|
||||||
const childModel = await childColumn.getModel(); |
|
||||||
await childModel.getColumns(); |
|
||||||
const parentModel = await parentColumn.getModel(); |
|
||||||
await parentModel.getColumns(); |
|
||||||
|
|
||||||
selectQb = knex( |
|
||||||
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`, |
|
||||||
).where( |
|
||||||
`${alias}.${parentColumn.column_name}`, |
|
||||||
knex.raw(`??`, [ |
|
||||||
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${ |
|
||||||
childColumn.column_name |
|
||||||
}`,
|
|
||||||
]), |
|
||||||
); |
|
||||||
} |
|
||||||
let lookupColumn = await lookup.getLookupColumn(); |
|
||||||
let prevAlias = alias; |
|
||||||
while (lookupColumn.uidt === UITypes.Lookup) { |
|
||||||
const nestedAlias = `__nc_lk_nested_${aliasCount++}`; |
|
||||||
const nestedLookup = await lookupColumn.getColOptions<LookupColumn>(); |
|
||||||
const relationCol = await nestedLookup.getRelationColumn(); |
|
||||||
const relation = |
|
||||||
await relationCol.getColOptions<LinkToAnotherRecordColumn>(); |
|
||||||
|
|
||||||
// if any of the relation in nested lookup is
|
|
||||||
// not belongs to then throw error as we don't support
|
|
||||||
if (relation.type !== RelationTypes.BELONGS_TO) |
|
||||||
NcError.badRequest('HasMany/ManyToMany lookup is not supported'); |
|
||||||
|
|
||||||
const childColumn = await relation.getChildColumn(); |
|
||||||
const parentColumn = await relation.getParentColumn(); |
|
||||||
const childModel = await childColumn.getModel(); |
|
||||||
await childModel.getColumns(); |
|
||||||
const parentModel = await parentColumn.getModel(); |
|
||||||
await parentModel.getColumns(); |
|
||||||
|
|
||||||
selectQb.join( |
|
||||||
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${nestedAlias}`, |
|
||||||
`${nestedAlias}.${parentColumn.column_name}`, |
|
||||||
`${prevAlias}.${childColumn.column_name}`, |
|
||||||
); |
|
||||||
|
|
||||||
lookupColumn = await nestedLookup.getLookupColumn(); |
|
||||||
prevAlias = nestedAlias; |
|
||||||
} |
|
||||||
|
|
||||||
switch (lookupColumn.uidt) { |
|
||||||
case UITypes.Links: |
|
||||||
case UITypes.Rollup: |
|
||||||
{ |
|
||||||
const builder = ( |
|
||||||
await genRollupSelectv2({ |
|
||||||
baseModelSqlv2, |
|
||||||
knex, |
|
||||||
columnOptions: |
|
||||||
(await lookupColumn.getColOptions()) as RollupColumn, |
|
||||||
alias: prevAlias, |
|
||||||
}) |
|
||||||
).builder; |
|
||||||
selectQb.select(builder); |
|
||||||
} |
|
||||||
break; |
|
||||||
case UITypes.LinkToAnotherRecord: |
|
||||||
{ |
|
||||||
const nestedAlias = `__nc_sort${aliasCount++}`; |
|
||||||
const relation = |
|
||||||
await lookupColumn.getColOptions<LinkToAnotherRecordColumn>(); |
|
||||||
if (relation.type !== 'bt') return; |
|
||||||
|
|
||||||
const colOptions = |
|
||||||
(await column.getColOptions()) as LinkToAnotherRecordColumn; |
|
||||||
const childColumn = await colOptions.getChildColumn(); |
|
||||||
const parentColumn = await colOptions.getParentColumn(); |
|
||||||
const childModel = await childColumn.getModel(); |
|
||||||
await childModel.getColumns(); |
|
||||||
const parentModel = await parentColumn.getModel(); |
|
||||||
await parentModel.getColumns(); |
|
||||||
|
|
||||||
selectQb |
|
||||||
.join( |
|
||||||
`${baseModelSqlv2.getTnPath( |
|
||||||
parentModel.table_name, |
|
||||||
)} as ${nestedAlias}`,
|
|
||||||
`${nestedAlias}.${parentColumn.column_name}`, |
|
||||||
`${prevAlias}.${childColumn.column_name}`, |
|
||||||
) |
|
||||||
.select(parentModel?.displayValue?.column_name); |
|
||||||
} |
|
||||||
break; |
|
||||||
case UITypes.Formula: |
|
||||||
{ |
|
||||||
const builder = ( |
|
||||||
await formulaQueryBuilderv2( |
|
||||||
baseModelSqlv2, |
|
||||||
( |
|
||||||
await column.getColOptions<FormulaColumn>() |
|
||||||
).formula, |
|
||||||
null, |
|
||||||
model, |
|
||||||
column, |
|
||||||
) |
|
||||||
).builder; |
|
||||||
|
|
||||||
selectQb.select(builder); |
|
||||||
} |
|
||||||
break; |
|
||||||
default: |
|
||||||
{ |
|
||||||
selectQb.select(`${prevAlias}.${lookupColumn.column_name}`); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
return { builder: selectQb }; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,399 @@ |
|||||||
|
import { RelationTypes, UITypes } from 'nocodb-sdk'; |
||||||
|
import type LookupColumn from '../models/LookupColumn'; |
||||||
|
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
||||||
|
import type { |
||||||
|
BarcodeColumn, |
||||||
|
Column, |
||||||
|
FormulaColumn, |
||||||
|
LinksColumn, |
||||||
|
LinkToAnotherRecordColumn, |
||||||
|
QrCodeColumn, |
||||||
|
RollupColumn, |
||||||
|
} from '~/models'; |
||||||
|
import { Model } from '~/models'; |
||||||
|
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2'; |
||||||
|
import genRollupSelectv2 from '~/db/genRollupSelectv2'; |
||||||
|
import { getAliasGenerator } from '~/utils'; |
||||||
|
import { NcError } from '~/helpers/catchError'; |
||||||
|
|
||||||
|
const LOOKUP_VAL_SEPARATOR = '___'; |
||||||
|
|
||||||
|
export async function getDisplayValueOfRefTable( |
||||||
|
relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>, |
||||||
|
) { |
||||||
|
return await relationCol |
||||||
|
.getColOptions() |
||||||
|
.then((colOpt) => colOpt.getRelatedTable()) |
||||||
|
.then((model) => model.getColumns()) |
||||||
|
.then((cols) => cols.find((col) => col.pv)); |
||||||
|
} |
||||||
|
|
||||||
|
export default async function generateLookupSelectQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
alias, |
||||||
|
model: _model, |
||||||
|
getAlias = getAliasGenerator('__lk_slt_'), |
||||||
|
}: { |
||||||
|
column: Column; |
||||||
|
baseModelSqlv2: BaseModelSqlv2; |
||||||
|
alias: string; |
||||||
|
model: Model; |
||||||
|
getAlias?: ReturnType<typeof getAliasGenerator>; |
||||||
|
}): Promise<any> { |
||||||
|
const knex = baseModelSqlv2.dbDriver; |
||||||
|
|
||||||
|
const rootAlias = alias; |
||||||
|
|
||||||
|
{ |
||||||
|
let selectQb; |
||||||
|
const alias = getAlias(); |
||||||
|
let lookupColOpt: LookupColumn; |
||||||
|
let isBtLookup = true; |
||||||
|
|
||||||
|
if (column.uidt === UITypes.Lookup) { |
||||||
|
lookupColOpt = await column.getColOptions<LookupColumn>(); |
||||||
|
} else if (column.uidt !== UITypes.LinkToAnotherRecord) { |
||||||
|
NcError.badRequest('Invalid field type'); |
||||||
|
} |
||||||
|
|
||||||
|
await column.getColOptions<LookupColumn>(); |
||||||
|
{ |
||||||
|
const relationCol = lookupColOpt |
||||||
|
? await lookupColOpt.getRelationColumn() |
||||||
|
: column; |
||||||
|
const relation = |
||||||
|
await relationCol.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
|
||||||
|
// if not belongs to then throw error as we don't support
|
||||||
|
if (relation.type === RelationTypes.BELONGS_TO) { |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
selectQb = knex( |
||||||
|
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`, |
||||||
|
).where( |
||||||
|
`${alias}.${parentColumn.column_name}`, |
||||||
|
knex.raw(`??`, [ |
||||||
|
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${ |
||||||
|
childColumn.column_name |
||||||
|
}`,
|
||||||
|
]), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// if not belongs to then throw error as we don't support
|
||||||
|
else if (relation.type === RelationTypes.HAS_MANY) { |
||||||
|
isBtLookup = false; |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
selectQb = knex( |
||||||
|
`${baseModelSqlv2.getTnPath(childModel.table_name)} as ${alias}`, |
||||||
|
).where( |
||||||
|
`${alias}.${childColumn.column_name}`, |
||||||
|
knex.raw(`??`, [ |
||||||
|
`${rootAlias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${ |
||||||
|
parentColumn.column_name |
||||||
|
}`,
|
||||||
|
]), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// if not belongs to then throw error as we don't support
|
||||||
|
else if (relation.type === RelationTypes.MANY_TO_MANY) { |
||||||
|
isBtLookup = false; |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
selectQb = knex( |
||||||
|
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`, |
||||||
|
); |
||||||
|
|
||||||
|
const mmTableAlias = getAlias(); |
||||||
|
|
||||||
|
const mmModel = await relation.getMMModel(); |
||||||
|
const mmChildCol = await relation.getMMChildColumn(); |
||||||
|
const mmParentCol = await relation.getMMParentColumn(); |
||||||
|
|
||||||
|
selectQb |
||||||
|
.innerJoin( |
||||||
|
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias), |
||||||
|
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`), |
||||||
|
'=', |
||||||
|
knex.ref(`${alias}.${parentColumn.column_name}`), |
||||||
|
) |
||||||
|
.where( |
||||||
|
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`), |
||||||
|
'=', |
||||||
|
knex.ref( |
||||||
|
`${ |
||||||
|
rootAlias || baseModelSqlv2.getTnPath(childModel.table_name) |
||||||
|
}.${childColumn.column_name}`,
|
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
let lookupColumn = lookupColOpt |
||||||
|
? await lookupColOpt.getLookupColumn() |
||||||
|
: await getDisplayValueOfRefTable(column); |
||||||
|
|
||||||
|
// if lookup column is qr code or barcode extract the referencing column
|
||||||
|
if ([UITypes.QrCode, UITypes.Barcode].includes(lookupColumn.uidt)) { |
||||||
|
lookupColumn = await lookupColumn |
||||||
|
.getColOptions<BarcodeColumn | QrCodeColumn>() |
||||||
|
.then((barcode) => barcode.getValueColumn()); |
||||||
|
} |
||||||
|
|
||||||
|
let prevAlias = alias; |
||||||
|
while ( |
||||||
|
lookupColumn.uidt === UITypes.Lookup || |
||||||
|
lookupColumn.uidt === UITypes.LinkToAnotherRecord |
||||||
|
) { |
||||||
|
const nestedAlias = getAlias(); |
||||||
|
|
||||||
|
let relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>; |
||||||
|
let nestedLookupColOpt: LookupColumn; |
||||||
|
|
||||||
|
if (lookupColumn.uidt === UITypes.Lookup) { |
||||||
|
nestedLookupColOpt = await lookupColumn.getColOptions<LookupColumn>(); |
||||||
|
relationCol = await nestedLookupColOpt.getRelationColumn(); |
||||||
|
} else { |
||||||
|
relationCol = lookupColumn; |
||||||
|
} |
||||||
|
|
||||||
|
const relation = |
||||||
|
await relationCol.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
|
||||||
|
// if any of the relation in nested lookupColOpt is
|
||||||
|
// not belongs to then throw error as we don't support
|
||||||
|
if (relation.type === RelationTypes.BELONGS_TO) { |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
selectQb.join( |
||||||
|
`${baseModelSqlv2.getTnPath( |
||||||
|
parentModel.table_name, |
||||||
|
)} as ${nestedAlias}`,
|
||||||
|
`${nestedAlias}.${parentColumn.column_name}`, |
||||||
|
`${prevAlias}.${childColumn.column_name}`, |
||||||
|
); |
||||||
|
} else if (relation.type === RelationTypes.HAS_MANY) { |
||||||
|
isBtLookup = false; |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
selectQb.join( |
||||||
|
`${baseModelSqlv2.getTnPath( |
||||||
|
childModel.table_name, |
||||||
|
)} as ${nestedAlias}`,
|
||||||
|
`${nestedAlias}.${childColumn.column_name}`, |
||||||
|
`${prevAlias}.${parentColumn.column_name}`, |
||||||
|
); |
||||||
|
} else if (relation.type === RelationTypes.MANY_TO_MANY) { |
||||||
|
isBtLookup = false; |
||||||
|
const childColumn = await relation.getChildColumn(); |
||||||
|
const parentColumn = await relation.getParentColumn(); |
||||||
|
const childModel = await childColumn.getModel(); |
||||||
|
await childModel.getColumns(); |
||||||
|
const parentModel = await parentColumn.getModel(); |
||||||
|
await parentModel.getColumns(); |
||||||
|
|
||||||
|
const mmTableAlias = getAlias(); |
||||||
|
|
||||||
|
const mmModel = await relation.getMMModel(); |
||||||
|
const mmChildCol = await relation.getMMChildColumn(); |
||||||
|
const mmParentCol = await relation.getMMParentColumn(); |
||||||
|
|
||||||
|
selectQb |
||||||
|
.innerJoin( |
||||||
|
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias), |
||||||
|
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`), |
||||||
|
'=', |
||||||
|
knex.ref(`${prevAlias}.${childColumn.column_name}`), |
||||||
|
) |
||||||
|
.innerJoin( |
||||||
|
knex.raw('?? as ??', [ |
||||||
|
baseModelSqlv2.getTnPath(parentModel.table_name), |
||||||
|
nestedAlias, |
||||||
|
]), |
||||||
|
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`), |
||||||
|
'=', |
||||||
|
knex.ref(`${nestedAlias}.${parentColumn.column_name}`), |
||||||
|
) |
||||||
|
.where( |
||||||
|
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`), |
||||||
|
'=', |
||||||
|
knex.ref( |
||||||
|
`${alias || baseModelSqlv2.getTnPath(childModel.table_name)}.${ |
||||||
|
childColumn.column_name |
||||||
|
}`,
|
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (lookupColumn.uidt === UITypes.Lookup) |
||||||
|
lookupColumn = await nestedLookupColOpt.getLookupColumn(); |
||||||
|
else lookupColumn = await getDisplayValueOfRefTable(relationCol); |
||||||
|
prevAlias = nestedAlias; |
||||||
|
} |
||||||
|
|
||||||
|
{ |
||||||
|
// get basemodel and model of lookup column
|
||||||
|
const model = await lookupColumn.getModel(); |
||||||
|
const baseModelSqlv2 = await Model.getBaseModelSQL({ |
||||||
|
model, |
||||||
|
dbDriver: knex, |
||||||
|
}); |
||||||
|
|
||||||
|
switch (lookupColumn.uidt) { |
||||||
|
case UITypes.Attachment: |
||||||
|
NcError.badRequest( |
||||||
|
'Group by using attachment column is not supported', |
||||||
|
); |
||||||
|
break; |
||||||
|
case UITypes.Links: |
||||||
|
case UITypes.Rollup: |
||||||
|
{ |
||||||
|
const builder = ( |
||||||
|
await genRollupSelectv2({ |
||||||
|
baseModelSqlv2, |
||||||
|
knex, |
||||||
|
columnOptions: |
||||||
|
(await lookupColumn.getColOptions()) as RollupColumn, |
||||||
|
alias: prevAlias, |
||||||
|
}) |
||||||
|
).builder; |
||||||
|
selectQb.select(builder); |
||||||
|
} |
||||||
|
break; |
||||||
|
case UITypes.Formula: |
||||||
|
{ |
||||||
|
const builder = ( |
||||||
|
await formulaQueryBuilderv2( |
||||||
|
baseModelSqlv2, |
||||||
|
( |
||||||
|
await lookupColumn.getColOptions<FormulaColumn>() |
||||||
|
).formula, |
||||||
|
lookupColumn.title, |
||||||
|
model, |
||||||
|
lookupColumn, |
||||||
|
await model.getAliasColMapping(), |
||||||
|
prevAlias, |
||||||
|
) |
||||||
|
).builder; |
||||||
|
|
||||||
|
selectQb.select(builder); |
||||||
|
} |
||||||
|
break; |
||||||
|
case UITypes.DateTime: |
||||||
|
{ |
||||||
|
await baseModelSqlv2.selectObject({ |
||||||
|
qb: selectQb, |
||||||
|
columns: [lookupColumn], |
||||||
|
alias: prevAlias, |
||||||
|
}); |
||||||
|
} |
||||||
|
break; |
||||||
|
default: |
||||||
|
{ |
||||||
|
selectQb.select( |
||||||
|
`${prevAlias}.${lookupColumn.column_name} as ${lookupColumn.title}`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
// if all relation are belongs to then we don't need to do the aggregation
|
||||||
|
if (isBtLookup) { |
||||||
|
return { |
||||||
|
builder: selectQb, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const subQueryAlias = getAlias(); |
||||||
|
|
||||||
|
if (baseModelSqlv2.isPg) { |
||||||
|
// alternate approach with array_agg
|
||||||
|
return { |
||||||
|
builder: knex |
||||||
|
.select(knex.raw('json_agg(??)::text', [lookupColumn.title])) |
||||||
|
.from(selectQb.as(subQueryAlias)), |
||||||
|
}; |
||||||
|
/* |
||||||
|
// alternate approach with array_agg
|
||||||
|
return { |
||||||
|
builder: knex |
||||||
|
.select(knex.raw('array_agg(??)', [lookupColumn.title])) |
||||||
|
.from(selectQb), |
||||||
|
};*/ |
||||||
|
// alternate approach with string aggregation
|
||||||
|
// return {
|
||||||
|
// builder: knex
|
||||||
|
// .select(
|
||||||
|
// knex.raw('STRING_AGG(??::text, ?)', [
|
||||||
|
// lookupColumn.title,
|
||||||
|
// LOOKUP_VAL_SEPARATOR,
|
||||||
|
// ]),
|
||||||
|
// )
|
||||||
|
// .from(selectQb.as(subQueryAlias)),
|
||||||
|
// };
|
||||||
|
} else if (baseModelSqlv2.isMySQL) { |
||||||
|
return { |
||||||
|
builder: knex |
||||||
|
.select( |
||||||
|
knex.raw('cast(JSON_ARRAYAGG(??) as NCHAR)', [lookupColumn.title]), |
||||||
|
) |
||||||
|
.from(selectQb.as(subQueryAlias)), |
||||||
|
}; |
||||||
|
|
||||||
|
// return {
|
||||||
|
// builder: knex
|
||||||
|
// .select(
|
||||||
|
// knex.raw('GROUP_CONCAT(?? ORDER BY ?? ASC SEPARATOR ?)', [
|
||||||
|
// lookupColumn.title,
|
||||||
|
// lookupColumn.title,
|
||||||
|
// LOOKUP_VAL_SEPARATOR,
|
||||||
|
// ]),
|
||||||
|
// )
|
||||||
|
// .from(selectQb.as(subQueryAlias)),
|
||||||
|
// };
|
||||||
|
} else if (baseModelSqlv2.isSqlite) { |
||||||
|
// ref: https://stackoverflow.com/questions/13382856/sqlite3-join-group-concat-using-distinct-with-custom-separator
|
||||||
|
// selectQb.orderBy(`${lookupColumn.title}`, 'asc');
|
||||||
|
return { |
||||||
|
builder: knex |
||||||
|
.select( |
||||||
|
knex.raw(`group_concat(??, ?)`, [ |
||||||
|
lookupColumn.title, |
||||||
|
LOOKUP_VAL_SEPARATOR, |
||||||
|
]), |
||||||
|
) |
||||||
|
.from(selectQb.as(subQueryAlias)), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
NcError.notImplemented('Database not supported Group by on Lookup'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import { getModelPaths } from './templates/paths'; |
||||||
|
import type { Model } from '~/models'; |
||||||
|
import type { SwaggerColumn } from './getSwaggerColumnMetas'; |
||||||
|
import type { SwaggerView } from './getSwaggerJSONV2'; |
||||||
|
import Noco from '~/Noco'; |
||||||
|
|
||||||
|
export default async function getPaths( |
||||||
|
{ |
||||||
|
model, |
||||||
|
columns, |
||||||
|
views, |
||||||
|
}: { |
||||||
|
model: Model; |
||||||
|
columns: SwaggerColumn[]; |
||||||
|
views: SwaggerView[]; |
||||||
|
}, |
||||||
|
_ncMeta = Noco.ncMeta, |
||||||
|
) { |
||||||
|
const swaggerPaths = await getModelPaths({ |
||||||
|
tableName: model.title, |
||||||
|
tableId: model.id, |
||||||
|
views, |
||||||
|
type: model.type, |
||||||
|
columns, |
||||||
|
}); |
||||||
|
|
||||||
|
return swaggerPaths; |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import { getModelSchemas } from './templates/schemas'; |
||||||
|
import type { Base, Model } from '~/models'; |
||||||
|
|
||||||
|
import type { SwaggerColumn } from './getSwaggerColumnMetas'; |
||||||
|
import type { SwaggerView } from './getSwaggerJSONV2'; |
||||||
|
import Noco from '~/Noco'; |
||||||
|
|
||||||
|
export default async function getSchemas( |
||||||
|
{ |
||||||
|
base, |
||||||
|
model, |
||||||
|
columns, |
||||||
|
}: { |
||||||
|
base: Base; |
||||||
|
model: Model; |
||||||
|
columns: SwaggerColumn[]; |
||||||
|
views: SwaggerView[]; |
||||||
|
}, |
||||||
|
_ncMeta = Noco.ncMeta, |
||||||
|
) { |
||||||
|
const swaggerSchemas = getModelSchemas({ |
||||||
|
tableName: model.title, |
||||||
|
orgs: 'v1', |
||||||
|
baseName: base.title, |
||||||
|
columns, |
||||||
|
}); |
||||||
|
|
||||||
|
return swaggerSchemas; |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
import { UITypes } from 'nocodb-sdk'; |
||||||
|
import type { Base, Column, LinkToAnotherRecordColumn } from '~/models'; |
||||||
|
import SwaggerTypes from '~/db/sql-mgr/code/routers/xc-ts/SwaggerTypes'; |
||||||
|
import Noco from '~/Noco'; |
||||||
|
|
||||||
|
export default async ( |
||||||
|
columns: Column[], |
||||||
|
base: Base, |
||||||
|
ncMeta = Noco.ncMeta, |
||||||
|
): Promise<SwaggerColumn[]> => { |
||||||
|
const dbType = await base.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: |
||||||
|
case UITypes.Links: |
||||||
|
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; |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
import { ViewTypes } from 'nocodb-sdk'; |
||||||
|
import swaggerBase from './swagger-base.json'; |
||||||
|
import getPaths from './getPaths'; |
||||||
|
import getSchemas from './getSchemas'; |
||||||
|
import getSwaggerColumnMetas from './getSwaggerColumnMetas'; |
||||||
|
import type { |
||||||
|
Base, |
||||||
|
FormViewColumn, |
||||||
|
GalleryViewColumn, |
||||||
|
GridViewColumn, |
||||||
|
Model, |
||||||
|
View, |
||||||
|
} from '~/models'; |
||||||
|
import Noco from '~/Noco'; |
||||||
|
|
||||||
|
export default async function getSwaggerJSONV2( |
||||||
|
base: Base, |
||||||
|
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), |
||||||
|
base, |
||||||
|
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({ model, columns, views }, ncMeta); |
||||||
|
|
||||||
|
const schemas = await getSchemas({ base, 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>; |
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
{ |
||||||
|
"openapi": "3.0.0", |
||||||
|
"info": { |
||||||
|
"title": "nocodb", |
||||||
|
"version": "2.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" |
||||||
|
} |
||||||
|
}, |
||||||
|
"responses": { |
||||||
|
"BadRequest": { |
||||||
|
"description": "BadReqeust", |
||||||
|
"content": { |
||||||
|
"application/json": { |
||||||
|
"schema": { |
||||||
|
"type": "object", |
||||||
|
"properties": { |
||||||
|
"msg": { |
||||||
|
"type": "string", |
||||||
|
"x-stoplight": { |
||||||
|
"id": "p9mk4oi0hbihm" |
||||||
|
}, |
||||||
|
"example": "BadRequest [Error]: <ERROR MESSAGE>" |
||||||
|
} |
||||||
|
}, |
||||||
|
"required": [ |
||||||
|
"msg" |
||||||
|
] |
||||||
|
}, |
||||||
|
"examples": { |
||||||
|
"Example 1": { |
||||||
|
"value": { |
||||||
|
"msg": "BadRequest [Error]: <ERROR MESSAGE>" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"headers": {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"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,237 @@ |
|||||||
|
import { isLinksOrLTAR, RelationTypes, UITypes } from 'nocodb-sdk'; |
||||||
|
import type { LinkToAnotherRecordColumn } from '~/models'; |
||||||
|
import type { SwaggerColumn } from '../getSwaggerColumnMetas'; |
||||||
|
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2'; |
||||||
|
|
||||||
|
export const recordIdParam = { |
||||||
|
schema: { |
||||||
|
type: 'string', |
||||||
|
}, |
||||||
|
name: 'recordId', |
||||||
|
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 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 linkFieldNameParam = (columns: SwaggerColumn[]) => { |
||||||
|
const linkColumnIds = []; |
||||||
|
const description = [ |
||||||
|
'**Links Field Identifier** corresponding to the relation field `Links` established between tables.\n\nLink Columns:', |
||||||
|
]; |
||||||
|
for (const { column } of columns) { |
||||||
|
if (!isLinksOrLTAR(column) || column.system) continue; |
||||||
|
linkColumnIds.push(column.id); |
||||||
|
|
||||||
|
description.push(`* ${column.id} - ${column.title}`); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
schema: { |
||||||
|
type: 'string', |
||||||
|
enum: linkColumnIds, |
||||||
|
}, |
||||||
|
name: 'linkFieldId', |
||||||
|
in: 'path', |
||||||
|
required: true, |
||||||
|
description: description.join('\n'), |
||||||
|
}; |
||||||
|
}; |
||||||
|
export const viewIdParams = (views: SwaggerView[]) => { |
||||||
|
const viewIds = []; |
||||||
|
const description = [ |
||||||
|
'Allows you to fetch records that are currently visible within a specific view.\n\nViews:', |
||||||
|
]; |
||||||
|
|
||||||
|
for (const { view } of views) { |
||||||
|
viewIds.push(view.id); |
||||||
|
description.push( |
||||||
|
`* ${view.id} - ${view.is_default ? 'Default view' : view.title}`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
schema: { |
||||||
|
type: 'string', |
||||||
|
enum: viewIds, |
||||||
|
}, |
||||||
|
description: description.join('\n'), |
||||||
|
name: 'viewId', |
||||||
|
in: 'query', |
||||||
|
required: false, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
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 && !column.system) { |
||||||
|
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([])); |
||||||
|
}; |
@ -0,0 +1,407 @@ |
|||||||
|
import { ModelTypes } from 'nocodb-sdk'; |
||||||
|
import { |
||||||
|
fieldsParam, |
||||||
|
getNestedParams, |
||||||
|
limitParam, |
||||||
|
linkFieldNameParam, |
||||||
|
offsetParam, |
||||||
|
recordIdParam, |
||||||
|
shuffleParam, |
||||||
|
sortParam, |
||||||
|
viewIdParams, |
||||||
|
whereParam, |
||||||
|
} from './params'; |
||||||
|
import type { SwaggerColumn } from '../getSwaggerColumnMetas'; |
||||||
|
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2'; |
||||||
|
import { isRelationExist } from '~/services/api-docs/swagger/templates/paths'; |
||||||
|
|
||||||
|
export const getModelPaths = async (ctx: { |
||||||
|
tableName: string; |
||||||
|
type: ModelTypes; |
||||||
|
columns: SwaggerColumn[]; |
||||||
|
tableId: string; |
||||||
|
views: SwaggerView[]; |
||||||
|
}): Promise<{ [path: string]: any }> => ({ |
||||||
|
[`/api/v2/tables/${ctx.tableId}/records`]: { |
||||||
|
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: [ |
||||||
|
viewIdParams(ctx.views), |
||||||
|
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`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
tags: [ctx.tableName], |
||||||
|
requestBody: { |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
oneOf: [ |
||||||
|
{ |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}Request`, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}Request`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
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: {}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
tags: [ctx.tableName], |
||||||
|
requestBody: { |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
oneOf: [ |
||||||
|
{ |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}Request`, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}Request`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
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.', |
||||||
|
requestBody: { |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
oneOf: [ |
||||||
|
{ |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}IdRequest`, |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}IdRequest`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
: {}), |
||||||
|
}, |
||||||
|
[`/api/v2/tables/${ctx.tableId}/records/{recordId}`]: { |
||||||
|
get: { |
||||||
|
parameters: [recordIdParam, fieldsParam], |
||||||
|
summary: `${ctx.tableName} read`, |
||||||
|
description: 'Read a row data by using the **primary key** column value.', |
||||||
|
operationId: `${ctx.tableName.toLowerCase()}-read`, |
||||||
|
tags: [ctx.tableName], |
||||||
|
responses: { |
||||||
|
'201': { |
||||||
|
description: 'Created', |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
$ref: `#/components/schemas/${ctx.tableName}Response`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
[`/api/v2/tables/${ctx.tableId}/records/count`]: { |
||||||
|
parameters: [viewIdParams(ctx.views)], |
||||||
|
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: { |
||||||
|
type: 'object', |
||||||
|
properties: { |
||||||
|
count: { |
||||||
|
type: 'number', |
||||||
|
}, |
||||||
|
}, |
||||||
|
required: ['list', 'pageInfo'], |
||||||
|
}, |
||||||
|
examples: { |
||||||
|
'Example 1': { |
||||||
|
value: { |
||||||
|
count: 3, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
...(isRelationExist(ctx.columns) |
||||||
|
? { |
||||||
|
[`/api/v2/tables/${ctx.tableId}/links/{linkFieldId}/records/{recordId}`]: |
||||||
|
{ |
||||||
|
parameters: [linkFieldNameParam(ctx.columns), recordIdParam], |
||||||
|
get: { |
||||||
|
summary: 'Link Records list', |
||||||
|
operationId: `${ctx.tableName.toLowerCase()}-nested-list`, |
||||||
|
description: |
||||||
|
'This API endpoint allows you to retrieve list of linked records for a specific `Link field` and `Record ID`. The response is an array of objects containing Primary Key and its corresponding display value.', |
||||||
|
tags: [ctx.tableName], |
||||||
|
parameters: [ |
||||||
|
fieldsParam, |
||||||
|
sortParam, |
||||||
|
whereParam, |
||||||
|
limitParam, |
||||||
|
offsetParam, |
||||||
|
], |
||||||
|
responses: { |
||||||
|
'200': { |
||||||
|
description: 'OK', |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
type: 'object', |
||||||
|
properties: { |
||||||
|
list: { |
||||||
|
type: 'array', |
||||||
|
description: 'List of data objects', |
||||||
|
items: { |
||||||
|
type: 'object', |
||||||
|
}, |
||||||
|
}, |
||||||
|
pageInfo: { |
||||||
|
$ref: '#/components/schemas/Paginated', |
||||||
|
description: 'Paginated Info', |
||||||
|
}, |
||||||
|
}, |
||||||
|
required: ['list', 'pageInfo'], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
post: { |
||||||
|
summary: 'Link Records', |
||||||
|
operationId: `${ctx.tableName.toLowerCase()}-nested-link`, |
||||||
|
responses: { |
||||||
|
'200': { |
||||||
|
description: 'OK', |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: {}, |
||||||
|
examples: { |
||||||
|
'Example 1': { |
||||||
|
value: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
tags: [ctx.tableName], |
||||||
|
requestBody: { |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
oneOf: [ |
||||||
|
{ |
||||||
|
type: 'object', |
||||||
|
}, |
||||||
|
{ |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
type: 'object', |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
examples: { |
||||||
|
'Example 1': { |
||||||
|
value: [ |
||||||
|
{ |
||||||
|
Id: 4, |
||||||
|
}, |
||||||
|
{ |
||||||
|
Id: 5, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
description: |
||||||
|
'This API endpoint allows you to link records to a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for linking purposes. Note that any existing links, if present, will be unaffected during this operation.', |
||||||
|
parameters: [recordIdParam], |
||||||
|
}, |
||||||
|
delete: { |
||||||
|
summary: 'Unlink Records', |
||||||
|
operationId: `${ctx.tableName.toLowerCase()}-nested-unlink`, |
||||||
|
responses: { |
||||||
|
'200': { |
||||||
|
description: 'OK', |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: {}, |
||||||
|
examples: { |
||||||
|
'Example 1': { |
||||||
|
value: true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
'400': { |
||||||
|
$ref: '#/components/responses/BadRequest', |
||||||
|
}, |
||||||
|
}, |
||||||
|
tags: [ctx.tableName], |
||||||
|
requestBody: { |
||||||
|
content: { |
||||||
|
'application/json': { |
||||||
|
schema: { |
||||||
|
oneOf: [ |
||||||
|
{ |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
type: 'object', |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
examples: { |
||||||
|
'Example 1': { |
||||||
|
value: [ |
||||||
|
{ |
||||||
|
Id: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
Id: 2, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
description: |
||||||
|
'This API endpoint allows you to unlink records from a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for unlinking purposes. Note that, \n- duplicated record-ids will be ignored.\n- non-existent record-ids will be ignored.', |
||||||
|
parameters: [recordIdParam], |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
: {}), |
||||||
|
}); |
||||||
|
|
||||||
|
function getPaginatedResponseType(type: string) { |
||||||
|
return { |
||||||
|
type: 'object', |
||||||
|
properties: { |
||||||
|
list: { |
||||||
|
type: 'array', |
||||||
|
items: { |
||||||
|
$ref: `#/components/schemas/${type}`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
PageInfo: { |
||||||
|
$ref: `#/components/schemas/Paginated`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
import { isSystemColumn } from 'nocodb-sdk'; |
||||||
|
import type { SwaggerColumn } from '../getSwaggerColumnMetas'; |
||||||
|
|
||||||
|
export const getModelSchemas = (ctx: { |
||||||
|
tableName: string; |
||||||
|
orgs: string; |
||||||
|
baseName: 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, |
||||||
|
...(column.system |
||||||
|
? {} |
||||||
|
: { |
||||||
|
[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 || isSystemColumn(column) || column.ai || column.meta?.ag |
||||||
|
? {} |
||||||
|
: { |
||||||
|
[title]: fieldProps, |
||||||
|
}), |
||||||
|
}), |
||||||
|
{}, |
||||||
|
) || {}), |
||||||
|
}, |
||||||
|
}, |
||||||
|
[`${ctx.tableName}IdRequest`]: { |
||||||
|
title: `${ctx.tableName} Id Request`, |
||||||
|
type: 'object', |
||||||
|
description: '', |
||||||
|
'x-internal': false, |
||||||
|
properties: { |
||||||
|
...(ctx.columns?.reduce( |
||||||
|
(colsObj, { title, virtual, column, ...fieldProps }) => ({ |
||||||
|
...colsObj, |
||||||
|
...(column.pk |
||||||
|
? { |
||||||
|
[title]: fieldProps, |
||||||
|
} |
||||||
|
: {}), |
||||||
|
}), |
||||||
|
{}, |
||||||
|
) || {}), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
export const getViewSchemas = (ctx: { |
||||||
|
tableName: string; |
||||||
|
viewName: string; |
||||||
|
orgs: string; |
||||||
|
baseName: 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, |
||||||
|
}), |
||||||
|
}), |
||||||
|
{}, |
||||||
|
) || {}), |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
Loading…
Reference in new issue