mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
9 changed files with 1137 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||||
|
import publicDataApis from './publicDataApis'; |
||||||
|
import publicDataExportApis from './publicDataExportApis'; |
||||||
|
import publicMetaApis from './publicMetaApis'; |
||||||
|
|
||||||
|
export { publicDataApis, publicDataExportApis, publicMetaApis }; |
@ -0,0 +1,104 @@ |
|||||||
|
import { Request, Response, Router } from 'express'; |
||||||
|
import multer from 'multer'; |
||||||
|
import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; |
||||||
|
import catchError from '../../meta/helpers/catchError'; |
||||||
|
import { publicDataService } from '../../services'; |
||||||
|
|
||||||
|
export async function dataList(req: Request, res: Response) { |
||||||
|
const pagedResponse = publicDataService.dataList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
}); |
||||||
|
res.json(pagedResponse); |
||||||
|
} |
||||||
|
|
||||||
|
// todo: Handle the error case where view doesnt belong to model
|
||||||
|
async function groupedDataList(req: Request, res: Response) { |
||||||
|
const groupedData = await publicDataService.groupedDataList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
groupColumnId: req.params.columnId, |
||||||
|
}); |
||||||
|
res.json(groupedData); |
||||||
|
} |
||||||
|
async function dataInsert(req: Request & { files: any[] }, res: Response) { |
||||||
|
const insertResult = await publicDataService.dataInsert({ |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
body: req.body?.data, |
||||||
|
siteUrl: (req as any).ncSiteUrl, |
||||||
|
files: req.files, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json(insertResult); |
||||||
|
} |
||||||
|
|
||||||
|
async function relDataList(req, res) { |
||||||
|
const pagedResponse = publicDataService.relDataList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
columnId: req.params.columnId, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json(pagedResponse); |
||||||
|
} |
||||||
|
|
||||||
|
export async function publicMmList(req: Request, res: Response) { |
||||||
|
const paginatedResponse = await publicDataService.publicMmList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
columnId: req.params.columnId, |
||||||
|
rowId: req.params.rowId, |
||||||
|
}); |
||||||
|
res.json(paginatedResponse); |
||||||
|
} |
||||||
|
|
||||||
|
export async function publicHmList(req: Request, res: Response) { |
||||||
|
const paginatedResponse = await publicDataService.publicHmList({ |
||||||
|
query: req.query, |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
columnId: req.params.columnId, |
||||||
|
rowId: req.params.rowId, |
||||||
|
}); |
||||||
|
res.json(paginatedResponse); |
||||||
|
} |
||||||
|
|
||||||
|
const router = Router({ mergeParams: true }); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/rows', |
||||||
|
catchError(dataList) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/group/:columnId', |
||||||
|
catchError(groupedDataList) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/nested/:columnId', |
||||||
|
catchError(relDataList) |
||||||
|
); |
||||||
|
router.post( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/rows', |
||||||
|
multer({ |
||||||
|
storage: multer.diskStorage({}), |
||||||
|
limits: { |
||||||
|
fieldSize: NC_ATTACHMENT_FIELD_SIZE, |
||||||
|
}, |
||||||
|
}).any(), |
||||||
|
catchError(dataInsert) |
||||||
|
); |
||||||
|
|
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:colId', |
||||||
|
catchError(publicMmList) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:colId', |
||||||
|
catchError(publicHmList) |
||||||
|
); |
||||||
|
|
||||||
|
export default router; |
@ -0,0 +1,266 @@ |
|||||||
|
import { Request, Response, Router } from 'express'; |
||||||
|
import * as XLSX from 'xlsx'; |
||||||
|
import { nocoExecute } from 'nc-help'; |
||||||
|
import papaparse from 'papaparse'; |
||||||
|
import { ErrorMessages, isSystemColumn, UITypes, ViewTypes } from 'nocodb-sdk'; |
||||||
|
import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; |
||||||
|
import catchError, { NcError } from '../../meta/helpers/catchError'; |
||||||
|
import { |
||||||
|
Base, |
||||||
|
Column, |
||||||
|
LinkToAnotherRecordColumn, |
||||||
|
LookupColumn, |
||||||
|
Model, |
||||||
|
View, |
||||||
|
} from '../../models'; |
||||||
|
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; |
||||||
|
|
||||||
|
async function exportExcel(req: Request, res: Response) { |
||||||
|
const view = await View.getByUUID(req.params.publicDataUuid); |
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if ( |
||||||
|
view.type !== ViewTypes.GRID && |
||||||
|
view.type !== ViewTypes.KANBAN && |
||||||
|
view.type !== ViewTypes.GALLERY && |
||||||
|
view.type !== ViewTypes.MAP |
||||||
|
) |
||||||
|
NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== req.headers?.['xc-password']) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const model = await view.getModelWithInfo(); |
||||||
|
|
||||||
|
await view.getColumns(); |
||||||
|
|
||||||
|
const { offset, dbRows, elapsed } = await getDbRows(model, view, req); |
||||||
|
|
||||||
|
const fields = req.query.fields as string[]; |
||||||
|
|
||||||
|
const data = XLSX.utils.json_to_sheet( |
||||||
|
dbRows.map((o: Record<string, any>) => |
||||||
|
Object.fromEntries(fields.map((f) => [f, o[f]])) |
||||||
|
), |
||||||
|
{ header: fields } |
||||||
|
); |
||||||
|
|
||||||
|
const wb = XLSX.utils.book_new(); |
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(wb, data, view.title); |
||||||
|
|
||||||
|
const buf = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' }); |
||||||
|
|
||||||
|
res.set({ |
||||||
|
'Access-Control-Expose-Headers': 'nc-export-offset', |
||||||
|
'nc-export-offset': offset, |
||||||
|
'nc-export-elapsed-time': elapsed, |
||||||
|
'Content-Disposition': `attachment; filename="${encodeURI( |
||||||
|
view.title |
||||||
|
)}-export.xlsx"`,
|
||||||
|
}); |
||||||
|
res.end(buf); |
||||||
|
} |
||||||
|
|
||||||
|
async function exportCsv(req: Request, res: Response) { |
||||||
|
const view = await View.getByUUID(req.params.publicDataUuid); |
||||||
|
const fields = req.query.fields; |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if ( |
||||||
|
view.type !== ViewTypes.GRID && |
||||||
|
view.type !== ViewTypes.KANBAN && |
||||||
|
view.type !== ViewTypes.GALLERY && |
||||||
|
view.type !== ViewTypes.MAP |
||||||
|
) |
||||||
|
NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== req.headers?.['xc-password']) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const model = await view.getModelWithInfo(); |
||||||
|
await view.getColumns(); |
||||||
|
|
||||||
|
const { offset, dbRows, elapsed } = await getDbRows(model, view, req); |
||||||
|
|
||||||
|
const data = papaparse.unparse( |
||||||
|
{ |
||||||
|
fields: model.columns |
||||||
|
.sort((c1, c2) => |
||||||
|
Array.isArray(fields) |
||||||
|
? fields.indexOf(c1.title as any) - fields.indexOf(c2.title as any) |
||||||
|
: 0 |
||||||
|
) |
||||||
|
.filter( |
||||||
|
(c) => |
||||||
|
!fields || !Array.isArray(fields) || fields.includes(c.title as any) |
||||||
|
) |
||||||
|
.map((c) => c.title), |
||||||
|
data: dbRows, |
||||||
|
}, |
||||||
|
{ |
||||||
|
escapeFormulae: true, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
res.set({ |
||||||
|
'Access-Control-Expose-Headers': 'nc-export-offset', |
||||||
|
'nc-export-offset': offset, |
||||||
|
'nc-export-elapsed-time': elapsed, |
||||||
|
'Content-Disposition': `attachment; filename="${encodeURI( |
||||||
|
view.title |
||||||
|
)}-export.csv"`,
|
||||||
|
}); |
||||||
|
res.send(data); |
||||||
|
} |
||||||
|
|
||||||
|
async function getDbRows(model, view: View, req: Request) { |
||||||
|
view.model.columns = view.columns |
||||||
|
.filter((c) => c.show) |
||||||
|
.map( |
||||||
|
(c) => |
||||||
|
new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any) |
||||||
|
) |
||||||
|
.filter((column) => !isSystemColumn(column) || view.show_system_fields); |
||||||
|
|
||||||
|
if (!model) NcError.notFound('Table not found'); |
||||||
|
|
||||||
|
const listArgs: any = { ...req.query }; |
||||||
|
try { |
||||||
|
listArgs.filterArr = JSON.parse(listArgs.filterArrJson); |
||||||
|
} catch (e) {} |
||||||
|
try { |
||||||
|
listArgs.sortArr = JSON.parse(listArgs.sortArrJson); |
||||||
|
} catch (e) {} |
||||||
|
|
||||||
|
const base = await Base.get(model.base_id); |
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: model.id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const requestObj = await getAst({ |
||||||
|
query: req.query, |
||||||
|
model, |
||||||
|
view, |
||||||
|
includePkByDefault: false, |
||||||
|
}); |
||||||
|
|
||||||
|
let offset = +req.query.offset || 0; |
||||||
|
const limit = 100; |
||||||
|
// const size = +process.env.NC_EXPORT_MAX_SIZE || 1024;
|
||||||
|
const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; |
||||||
|
const dbRows = []; |
||||||
|
const startTime = process.hrtime(); |
||||||
|
let elapsed, temp; |
||||||
|
|
||||||
|
for ( |
||||||
|
elapsed = 0; |
||||||
|
elapsed < timeout; |
||||||
|
offset += limit, |
||||||
|
temp = process.hrtime(startTime), |
||||||
|
elapsed = temp[0] * 1000 + temp[1] / 1000000 |
||||||
|
) { |
||||||
|
const rows = await nocoExecute( |
||||||
|
requestObj, |
||||||
|
await baseModel.list({ ...listArgs, offset, limit }), |
||||||
|
{}, |
||||||
|
listArgs |
||||||
|
); |
||||||
|
|
||||||
|
if (!rows?.length) { |
||||||
|
offset = -1; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
for (const row of rows) { |
||||||
|
const dbRow = { ...row }; |
||||||
|
|
||||||
|
for (const column of view.model.columns) { |
||||||
|
dbRow[column.title] = await serializeCellValue({ |
||||||
|
value: row[column.title], |
||||||
|
column, |
||||||
|
}); |
||||||
|
} |
||||||
|
dbRows.push(dbRow); |
||||||
|
} |
||||||
|
} |
||||||
|
return { offset, dbRows, elapsed }; |
||||||
|
} |
||||||
|
|
||||||
|
async function serializeCellValue({ |
||||||
|
value, |
||||||
|
column, |
||||||
|
}: { |
||||||
|
column?: Column; |
||||||
|
value: any; |
||||||
|
}) { |
||||||
|
if (!column) { |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
if (!value) return value; |
||||||
|
|
||||||
|
switch (column?.uidt) { |
||||||
|
case UITypes.Attachment: { |
||||||
|
let data = value; |
||||||
|
try { |
||||||
|
if (typeof value === 'string') { |
||||||
|
data = JSON.parse(value); |
||||||
|
} |
||||||
|
} catch {} |
||||||
|
|
||||||
|
return (data || []).map( |
||||||
|
(attachment) => |
||||||
|
`${encodeURI(attachment.title)}(${encodeURI(attachment.url)})` |
||||||
|
); |
||||||
|
} |
||||||
|
case UITypes.Lookup: |
||||||
|
{ |
||||||
|
const colOptions = await column.getColOptions<LookupColumn>(); |
||||||
|
const lookupColumn = await colOptions.getLookupColumn(); |
||||||
|
return ( |
||||||
|
await Promise.all( |
||||||
|
[...(Array.isArray(value) ? value : [value])].map(async (v) => |
||||||
|
serializeCellValue({ |
||||||
|
value: v, |
||||||
|
column: lookupColumn, |
||||||
|
}) |
||||||
|
) |
||||||
|
) |
||||||
|
).join(', '); |
||||||
|
} |
||||||
|
break; |
||||||
|
case UITypes.LinkToAnotherRecord: |
||||||
|
{ |
||||||
|
const colOptions = |
||||||
|
await column.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
const relatedModel = await colOptions.getRelatedTable(); |
||||||
|
await relatedModel.getColumns(); |
||||||
|
return [...(Array.isArray(value) ? value : [value])] |
||||||
|
.map((v) => { |
||||||
|
return v[relatedModel.displayValue?.title]; |
||||||
|
}) |
||||||
|
.join(', '); |
||||||
|
} |
||||||
|
break; |
||||||
|
default: |
||||||
|
if (value && typeof value === 'object') { |
||||||
|
return JSON.stringify(value); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const router = Router({ mergeParams: true }); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:publicDataUuid/rows/export/csv', |
||||||
|
catchError(exportCsv) |
||||||
|
); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:publicDataUuid/rows/export/excel', |
||||||
|
catchError(exportExcel) |
||||||
|
); |
||||||
|
export default router; |
@ -0,0 +1,31 @@ |
|||||||
|
import { Request, Response, Router } from 'express'; |
||||||
|
import catchError from '../../meta/helpers/catchError'; |
||||||
|
import { publicMetaService } from '../../services'; |
||||||
|
|
||||||
|
export async function viewMetaGet(req: Request, res: Response) { |
||||||
|
res.json( |
||||||
|
await publicMetaService.viewMetaGet({ |
||||||
|
password: req.headers?.['xc-password'] as string, |
||||||
|
sharedViewUuid: req.params.sharedViewUuid, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
async function publicSharedBaseGet(req, res): Promise<any> { |
||||||
|
res.json( |
||||||
|
await publicMetaService.publicSharedBaseGet({ |
||||||
|
sharedBaseUuid: req.params.sharedViewUuid, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const router = Router({ mergeParams: true }); |
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-view/:sharedViewUuid/meta', |
||||||
|
catchError(viewMetaGet) |
||||||
|
); |
||||||
|
|
||||||
|
router.get( |
||||||
|
'/api/v1/db/public/shared-base/:sharedBaseUuid/meta', |
||||||
|
catchError(publicSharedBaseGet) |
||||||
|
); |
||||||
|
export default router; |
@ -0,0 +1,5 @@ |
|||||||
|
import * as publicDataService from './publicDataService'; |
||||||
|
import * as publicDataExportApis from './publicDataExportService'; |
||||||
|
import * as publicMetaService from './publicMetaservice'; |
||||||
|
|
||||||
|
export { publicDataService, publicDataExportApis, publicMetaService }; |
@ -0,0 +1,162 @@ |
|||||||
|
import { nocoExecute } from 'nc-help'; |
||||||
|
import { isSystemColumn, UITypes } from 'nocodb-sdk'; |
||||||
|
import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; |
||||||
|
import { NcError } from '../../meta/helpers/catchError'; |
||||||
|
import { |
||||||
|
Base, |
||||||
|
Column, |
||||||
|
LinkToAnotherRecordColumn, |
||||||
|
LookupColumn, |
||||||
|
Model, |
||||||
|
View, |
||||||
|
} from '../../models'; |
||||||
|
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; |
||||||
|
|
||||||
|
export async function getDbRows(param: { |
||||||
|
model; |
||||||
|
view: View; |
||||||
|
query: any; |
||||||
|
offset?: number; |
||||||
|
}) { |
||||||
|
param.view.model.columns = param.view.columns |
||||||
|
.filter((c) => c.show) |
||||||
|
.map( |
||||||
|
(c) => |
||||||
|
new Column({ |
||||||
|
...c, |
||||||
|
...param.view.model.columnsById[c.fk_column_id], |
||||||
|
} as any) |
||||||
|
) |
||||||
|
.filter( |
||||||
|
(column) => !isSystemColumn(column) || param.view.show_system_fields |
||||||
|
); |
||||||
|
|
||||||
|
if (!param.model) NcError.notFound('Table not found'); |
||||||
|
|
||||||
|
const listArgs: any = { ...param.query }; |
||||||
|
try { |
||||||
|
listArgs.filterArr = JSON.parse(listArgs.filterArrJson); |
||||||
|
} catch (e) {} |
||||||
|
try { |
||||||
|
listArgs.sortArr = JSON.parse(listArgs.sortArrJson); |
||||||
|
} catch (e) {} |
||||||
|
|
||||||
|
const base = await Base.get(param.model.base_id); |
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: param.model.id, |
||||||
|
viewId: param.view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const requestObj = await getAst({ |
||||||
|
query: param.query, |
||||||
|
model: param.model, |
||||||
|
view: param.view, |
||||||
|
includePkByDefault: false, |
||||||
|
}); |
||||||
|
|
||||||
|
let offset = +param.offset || 0; |
||||||
|
const limit = 100; |
||||||
|
// const size = +process.env.NC_EXPORT_MAX_SIZE || 1024;
|
||||||
|
const timeout = +process.env.NC_EXPORT_MAX_TIMEOUT || 5000; |
||||||
|
const dbRows = []; |
||||||
|
const startTime = process.hrtime(); |
||||||
|
let elapsed, temp; |
||||||
|
|
||||||
|
for ( |
||||||
|
elapsed = 0; |
||||||
|
elapsed < timeout; |
||||||
|
offset += limit, |
||||||
|
temp = process.hrtime(startTime), |
||||||
|
elapsed = temp[0] * 1000 + temp[1] / 1000000 |
||||||
|
) { |
||||||
|
const rows = await nocoExecute( |
||||||
|
requestObj, |
||||||
|
await baseModel.list({ ...listArgs, offset, limit }), |
||||||
|
{}, |
||||||
|
listArgs |
||||||
|
); |
||||||
|
|
||||||
|
if (!rows?.length) { |
||||||
|
offset = -1; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
for (const row of rows) { |
||||||
|
const dbRow = { ...row }; |
||||||
|
|
||||||
|
for (const column of param.view.model.columns) { |
||||||
|
dbRow[column.title] = await serializeCellValue({ |
||||||
|
value: row[column.title], |
||||||
|
column, |
||||||
|
}); |
||||||
|
} |
||||||
|
dbRows.push(dbRow); |
||||||
|
} |
||||||
|
} |
||||||
|
return { offset, dbRows, elapsed }; |
||||||
|
} |
||||||
|
|
||||||
|
export async function serializeCellValue({ |
||||||
|
value, |
||||||
|
column, |
||||||
|
}: { |
||||||
|
column?: Column; |
||||||
|
value: any; |
||||||
|
}) { |
||||||
|
if (!column) { |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
if (!value) return value; |
||||||
|
|
||||||
|
switch (column?.uidt) { |
||||||
|
case UITypes.Attachment: { |
||||||
|
let data = value; |
||||||
|
try { |
||||||
|
if (typeof value === 'string') { |
||||||
|
data = JSON.parse(value); |
||||||
|
} |
||||||
|
} catch {} |
||||||
|
|
||||||
|
return (data || []).map( |
||||||
|
(attachment) => |
||||||
|
`${encodeURI(attachment.title)}(${encodeURI(attachment.url)})` |
||||||
|
); |
||||||
|
} |
||||||
|
case UITypes.Lookup: |
||||||
|
{ |
||||||
|
const colOptions = await column.getColOptions<LookupColumn>(); |
||||||
|
const lookupColumn = await colOptions.getLookupColumn(); |
||||||
|
return ( |
||||||
|
await Promise.all( |
||||||
|
[...(Array.isArray(value) ? value : [value])].map(async (v) => |
||||||
|
serializeCellValue({ |
||||||
|
value: v, |
||||||
|
column: lookupColumn, |
||||||
|
}) |
||||||
|
) |
||||||
|
) |
||||||
|
).join(', '); |
||||||
|
} |
||||||
|
break; |
||||||
|
case UITypes.LinkToAnotherRecord: |
||||||
|
{ |
||||||
|
const colOptions = |
||||||
|
await column.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
const relatedModel = await colOptions.getRelatedTable(); |
||||||
|
await relatedModel.getColumns(); |
||||||
|
return [...(Array.isArray(value) ? value : [value])] |
||||||
|
.map((v) => { |
||||||
|
return v[relatedModel.displayValue?.title]; |
||||||
|
}) |
||||||
|
.join(', '); |
||||||
|
} |
||||||
|
break; |
||||||
|
default: |
||||||
|
if (value && typeof value === 'object') { |
||||||
|
return JSON.stringify(value); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,463 @@ |
|||||||
|
import { nocoExecute } from 'nc-help'; |
||||||
|
import { ErrorMessages, UITypes, ViewTypes } from 'nocodb-sdk'; |
||||||
|
import path from 'path'; |
||||||
|
import { nanoid } from 'nanoid'; |
||||||
|
import slash from 'slash'; |
||||||
|
import getAst from '../../db/sql-data-mapper/lib/sql/helpers/getAst'; |
||||||
|
import { NcError } from '../../meta/helpers/catchError'; |
||||||
|
import NcPluginMgrv2 from '../../meta/helpers/NcPluginMgrv2'; |
||||||
|
import { PagedResponseImpl } from '../../meta/helpers/PagedResponse'; |
||||||
|
import { |
||||||
|
Base, |
||||||
|
Column, |
||||||
|
LinkToAnotherRecordColumn, |
||||||
|
Model, |
||||||
|
View, |
||||||
|
} from '../../models'; |
||||||
|
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; |
||||||
|
import { mimeIcons } from '../../utils/mimeTypes'; |
||||||
|
import { sanitizeUrlPath } from '../attachmentService'; |
||||||
|
import { getColumnByIdOrName } from '../dataService/helpers'; |
||||||
|
|
||||||
|
export async function dataList(param: { |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
query: any; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if ( |
||||||
|
view.type !== ViewTypes.GRID && |
||||||
|
view.type !== ViewTypes.KANBAN && |
||||||
|
view.type !== ViewTypes.GALLERY && |
||||||
|
view.type !== ViewTypes.MAP |
||||||
|
) { |
||||||
|
NcError.notFound('Not found'); |
||||||
|
} |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const model = await Model.getByIdOrName({ |
||||||
|
id: view?.fk_model_id, |
||||||
|
}); |
||||||
|
|
||||||
|
const base = await Base.get(model.base_id); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: model.id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const listArgs: any = { ...param.query }; |
||||||
|
try { |
||||||
|
listArgs.filterArr = JSON.parse(listArgs.filterArrJson); |
||||||
|
} catch (e) {} |
||||||
|
try { |
||||||
|
listArgs.sortArr = JSON.parse(listArgs.sortArrJson); |
||||||
|
} catch (e) {} |
||||||
|
|
||||||
|
let data = []; |
||||||
|
let count = 0; |
||||||
|
|
||||||
|
try { |
||||||
|
data = await nocoExecute( |
||||||
|
await getAst({ |
||||||
|
query: param.query, |
||||||
|
model, |
||||||
|
view, |
||||||
|
}), |
||||||
|
await baseModel.list(listArgs), |
||||||
|
{}, |
||||||
|
listArgs |
||||||
|
); |
||||||
|
count = await baseModel.count(listArgs); |
||||||
|
} catch (e) { |
||||||
|
// show empty result instead of throwing error here
|
||||||
|
// e.g. search some text in a numeric field
|
||||||
|
} |
||||||
|
|
||||||
|
return new PagedResponseImpl(data, { ...param.query, count }); |
||||||
|
} |
||||||
|
|
||||||
|
// todo: Handle the error case where view doesnt belong to model
|
||||||
|
export async function groupedDataList(param: { |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
query: any; |
||||||
|
groupColumnId: string; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if ( |
||||||
|
view.type !== ViewTypes.GRID && |
||||||
|
view.type !== ViewTypes.KANBAN && |
||||||
|
view.type !== ViewTypes.GALLERY |
||||||
|
) { |
||||||
|
NcError.notFound('Not found'); |
||||||
|
} |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const model = await Model.getByIdOrName({ |
||||||
|
id: view?.fk_model_id, |
||||||
|
}); |
||||||
|
|
||||||
|
return await getGroupedDataList({ |
||||||
|
model, |
||||||
|
view, |
||||||
|
query: param.query, |
||||||
|
groupColumnId: param.groupColumnId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async function getGroupedDataList(param: { |
||||||
|
model: Model; |
||||||
|
view: View; |
||||||
|
query: any; |
||||||
|
groupColumnId: string; |
||||||
|
}) { |
||||||
|
const { model, view, query = {}, groupColumnId } = param; |
||||||
|
const base = await Base.get(param.model.base_id); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: model.id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const requestObj = await getAst({ model, query: param.query, view }); |
||||||
|
|
||||||
|
const listArgs: any = { ...query }; |
||||||
|
try { |
||||||
|
listArgs.filterArr = JSON.parse(listArgs.filterArrJson); |
||||||
|
} catch (e) {} |
||||||
|
try { |
||||||
|
listArgs.sortArr = JSON.parse(listArgs.sortArrJson); |
||||||
|
} catch (e) {} |
||||||
|
try { |
||||||
|
listArgs.options = JSON.parse(listArgs.optionsArrJson); |
||||||
|
} catch (e) {} |
||||||
|
|
||||||
|
let data = []; |
||||||
|
|
||||||
|
try { |
||||||
|
const groupedData = await baseModel.groupedList({ |
||||||
|
...listArgs, |
||||||
|
groupColumnId, |
||||||
|
}); |
||||||
|
data = await nocoExecute( |
||||||
|
{ key: 1, value: requestObj }, |
||||||
|
groupedData, |
||||||
|
{}, |
||||||
|
listArgs |
||||||
|
); |
||||||
|
const countArr = await baseModel.groupedListCount({ |
||||||
|
...listArgs, |
||||||
|
groupColumnId, |
||||||
|
}); |
||||||
|
data = data.map((item) => { |
||||||
|
// todo: use map to avoid loop
|
||||||
|
const count = |
||||||
|
countArr.find((countItem: any) => countItem.key === item.key)?.count ?? |
||||||
|
0; |
||||||
|
|
||||||
|
item.value = new PagedResponseImpl(item.value, { |
||||||
|
...query, |
||||||
|
count: count, |
||||||
|
}); |
||||||
|
return item; |
||||||
|
}); |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
NcError.internalServerError('Internal Server Error'); |
||||||
|
} |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
export async function dataInsert(param: { |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
body: any; |
||||||
|
files: any[]; |
||||||
|
siteUrl: string; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound(); |
||||||
|
if (view.type !== ViewTypes.FORM) NcError.notFound(); |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
return NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const model = await Model.getByIdOrName({ |
||||||
|
id: view?.fk_model_id, |
||||||
|
}); |
||||||
|
|
||||||
|
const base = await Base.get(model.base_id); |
||||||
|
const project = await base.getProject(); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: model.id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
await view.getViewWithInfo(); |
||||||
|
await view.getColumns(); |
||||||
|
await view.getModelWithInfo(); |
||||||
|
await view.model.getColumns(); |
||||||
|
|
||||||
|
const fields = (view.model.columns = view.columns |
||||||
|
.filter((c) => c.show) |
||||||
|
.reduce((o, c) => { |
||||||
|
o[view.model.columnsById[c.fk_column_id].title] = new Column({ |
||||||
|
...c, |
||||||
|
...view.model.columnsById[c.fk_column_id], |
||||||
|
} as any); |
||||||
|
return o; |
||||||
|
}, {}) as any); |
||||||
|
|
||||||
|
let body = param?.body; |
||||||
|
|
||||||
|
if (typeof body === 'string') body = JSON.parse(body); |
||||||
|
|
||||||
|
const insertObject = Object.entries(body).reduce((obj, [key, val]) => { |
||||||
|
if (key in fields) { |
||||||
|
obj[key] = val; |
||||||
|
} |
||||||
|
return obj; |
||||||
|
}, {}); |
||||||
|
|
||||||
|
const attachments = {}; |
||||||
|
const storageAdapter = await NcPluginMgrv2.storageAdapter(); |
||||||
|
|
||||||
|
for (const file of param.files || []) { |
||||||
|
// remove `_` prefix and `[]` suffix
|
||||||
|
const fieldName = file?.fieldname?.replace(/^_|\[\d*]$/g, ''); |
||||||
|
|
||||||
|
const filePath = sanitizeUrlPath([ |
||||||
|
'v1', |
||||||
|
project.title, |
||||||
|
model.title, |
||||||
|
fieldName, |
||||||
|
]); |
||||||
|
|
||||||
|
if (fieldName in fields && fields[fieldName].uidt === UITypes.Attachment) { |
||||||
|
attachments[fieldName] = attachments[fieldName] || []; |
||||||
|
const fileName = `${nanoid(6)}_${file.originalname}`; |
||||||
|
let url = await storageAdapter.fileCreate( |
||||||
|
slash(path.join('nc', 'uploads', ...filePath, fileName)), |
||||||
|
file |
||||||
|
); |
||||||
|
|
||||||
|
if (!url) { |
||||||
|
url = `${param.siteUrl}/download/${filePath.join('/')}/${fileName}`; |
||||||
|
} |
||||||
|
|
||||||
|
attachments[fieldName].push({ |
||||||
|
url, |
||||||
|
title: file.originalname, |
||||||
|
mimetype: file.mimetype, |
||||||
|
size: file.size, |
||||||
|
icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (const [column, data] of Object.entries(attachments)) { |
||||||
|
insertObject[column] = JSON.stringify(data); |
||||||
|
} |
||||||
|
|
||||||
|
return await baseModel.nestedInsert(insertObject, null); |
||||||
|
} |
||||||
|
|
||||||
|
export async function relDataList(param: { |
||||||
|
query: any; |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
columnId: string; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.type !== ViewTypes.FORM) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const column = await Column.get({ colId: param.columnId }); |
||||||
|
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
|
||||||
|
const model = await colOptions.getRelatedTable(); |
||||||
|
|
||||||
|
const base = await Base.get(model.base_id); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: model.id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const requestObj = await getAst({ |
||||||
|
query: param.query, |
||||||
|
model, |
||||||
|
extractOnlyPrimaries: true, |
||||||
|
}); |
||||||
|
|
||||||
|
let data = []; |
||||||
|
let count = 0; |
||||||
|
try { |
||||||
|
data = data = await nocoExecute( |
||||||
|
requestObj, |
||||||
|
await baseModel.list(param.query), |
||||||
|
{}, |
||||||
|
param.query |
||||||
|
); |
||||||
|
count = await baseModel.count(param.query); |
||||||
|
} catch (e) { |
||||||
|
// show empty result instead of throwing error here
|
||||||
|
// e.g. search some text in a numeric field
|
||||||
|
} |
||||||
|
|
||||||
|
return new PagedResponseImpl(data, { ...param.query, count }); |
||||||
|
} |
||||||
|
|
||||||
|
export async function publicMmList(param: { |
||||||
|
query: any; |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
columnId: string; |
||||||
|
rowId: string; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const column = await getColumnByIdOrName( |
||||||
|
param.columnId, |
||||||
|
await view.getModel() |
||||||
|
); |
||||||
|
|
||||||
|
if (column.fk_model_id !== view.fk_model_id) |
||||||
|
NcError.badRequest("Column doesn't belongs to the model"); |
||||||
|
|
||||||
|
const base = await Base.get(view.base_id); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: view.fk_model_id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const key = `List`; |
||||||
|
const requestObj: any = { |
||||||
|
[key]: 1, |
||||||
|
}; |
||||||
|
|
||||||
|
const data = ( |
||||||
|
await nocoExecute( |
||||||
|
requestObj, |
||||||
|
{ |
||||||
|
[key]: async (args) => { |
||||||
|
return await baseModel.mmList( |
||||||
|
{ |
||||||
|
colId: param.columnId, |
||||||
|
parentId: param.rowId, |
||||||
|
}, |
||||||
|
args |
||||||
|
); |
||||||
|
}, |
||||||
|
}, |
||||||
|
{}, |
||||||
|
|
||||||
|
{ nested: { [key]: param.query } } |
||||||
|
) |
||||||
|
)?.[key]; |
||||||
|
|
||||||
|
const count: any = await baseModel.mmListCount({ |
||||||
|
colId: param.columnId, |
||||||
|
parentId: param.rowId, |
||||||
|
}); |
||||||
|
|
||||||
|
return new PagedResponseImpl(data, { ...param.query, count }); |
||||||
|
} |
||||||
|
|
||||||
|
export async function publicHmList(param: { |
||||||
|
query: any; |
||||||
|
rowId: string; |
||||||
|
sharedViewUuid: string; |
||||||
|
password?: string; |
||||||
|
columnId: string; |
||||||
|
}) { |
||||||
|
const view = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
if (view.type !== ViewTypes.GRID) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
const column = await getColumnByIdOrName( |
||||||
|
param.columnId, |
||||||
|
await view.getModel() |
||||||
|
); |
||||||
|
|
||||||
|
if (column.fk_model_id !== view.fk_model_id) |
||||||
|
NcError.badRequest("Column doesn't belongs to the model"); |
||||||
|
|
||||||
|
const base = await Base.get(view.base_id); |
||||||
|
|
||||||
|
const baseModel = await Model.getBaseModelSQL({ |
||||||
|
id: view.fk_model_id, |
||||||
|
viewId: view?.id, |
||||||
|
dbDriver: NcConnectionMgrv2.get(base), |
||||||
|
}); |
||||||
|
|
||||||
|
const key = `List`; |
||||||
|
const requestObj: any = { |
||||||
|
[key]: 1, |
||||||
|
}; |
||||||
|
|
||||||
|
const data = ( |
||||||
|
await nocoExecute( |
||||||
|
requestObj, |
||||||
|
{ |
||||||
|
[key]: async (args) => { |
||||||
|
return await baseModel.hmList( |
||||||
|
{ |
||||||
|
colId: param.columnId, |
||||||
|
id: param.rowId, |
||||||
|
}, |
||||||
|
args |
||||||
|
); |
||||||
|
}, |
||||||
|
}, |
||||||
|
{}, |
||||||
|
{ nested: { [key]: param.query } } |
||||||
|
) |
||||||
|
)?.[key]; |
||||||
|
|
||||||
|
const count = await baseModel.hmListCount({ |
||||||
|
colId: param.columnId, |
||||||
|
id: param.rowId, |
||||||
|
}); |
||||||
|
|
||||||
|
return new PagedResponseImpl(data, { ...param.query, count }); |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
import { |
||||||
|
ErrorMessages, |
||||||
|
LinkToAnotherRecordType, |
||||||
|
RelationTypes, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import { NcError } from '../../meta/helpers/catchError'; |
||||||
|
import { |
||||||
|
Base, |
||||||
|
Column, |
||||||
|
LinkToAnotherRecordColumn, |
||||||
|
Model, |
||||||
|
Project, |
||||||
|
View, |
||||||
|
} from '../../models'; |
||||||
|
|
||||||
|
export async function viewMetaGet(param: { |
||||||
|
sharedViewUuid: string; |
||||||
|
password: string; |
||||||
|
}) { |
||||||
|
const view: View & { |
||||||
|
relatedMetas?: { [ket: string]: Model }; |
||||||
|
client?: string; |
||||||
|
} = await View.getByUUID(param.sharedViewUuid); |
||||||
|
|
||||||
|
if (!view) NcError.notFound('Not found'); |
||||||
|
|
||||||
|
if (view.password && view.password !== param.password) { |
||||||
|
NcError.forbidden(ErrorMessages.INVALID_SHARED_VIEW_PASSWORD); |
||||||
|
} |
||||||
|
|
||||||
|
await view.getFilters(); |
||||||
|
await view.getSorts(); |
||||||
|
|
||||||
|
await view.getViewWithInfo(); |
||||||
|
await view.getColumns(); |
||||||
|
await view.getModelWithInfo(); |
||||||
|
await view.model.getColumns(); |
||||||
|
|
||||||
|
const base = await Base.get(view.model.base_id); |
||||||
|
view.client = base.type; |
||||||
|
|
||||||
|
// todo: return only required props
|
||||||
|
delete view['password']; |
||||||
|
|
||||||
|
view.model.columns = view.columns |
||||||
|
.filter((c) => { |
||||||
|
const column = view.model.columnsById[c.fk_column_id]; |
||||||
|
return ( |
||||||
|
c.show || |
||||||
|
(column.rqd && !column.cdf && !column.ai) || |
||||||
|
column.pk || |
||||||
|
view.model.columns.some( |
||||||
|
(c1) => |
||||||
|
c1.uidt === UITypes.LinkToAnotherRecord && |
||||||
|
(<LinkToAnotherRecordColumn>c1.colOptions).type === |
||||||
|
RelationTypes.BELONGS_TO && |
||||||
|
view.columns.some((vc) => vc.fk_column_id === c1.id && vc.show) && |
||||||
|
(<LinkToAnotherRecordColumn>c1.colOptions).fk_child_column_id === |
||||||
|
c.fk_column_id |
||||||
|
) |
||||||
|
); |
||||||
|
}) |
||||||
|
.map( |
||||||
|
(c) => |
||||||
|
new Column({ ...c, ...view.model.columnsById[c.fk_column_id] } as any) |
||||||
|
) as any; |
||||||
|
|
||||||
|
const relatedMetas = {}; |
||||||
|
|
||||||
|
// load related table metas
|
||||||
|
for (const col of view.model.columns) { |
||||||
|
if (UITypes.LinkToAnotherRecord === col.uidt) { |
||||||
|
const colOpt = await col.getColOptions<LinkToAnotherRecordType>(); |
||||||
|
relatedMetas[colOpt.fk_related_model_id] = await Model.getWithInfo({ |
||||||
|
id: colOpt.fk_related_model_id, |
||||||
|
}); |
||||||
|
if (colOpt.type === 'mm') { |
||||||
|
relatedMetas[colOpt.fk_mm_model_id] = await Model.getWithInfo({ |
||||||
|
id: colOpt.fk_mm_model_id, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
view.relatedMetas = relatedMetas; |
||||||
|
|
||||||
|
return view; |
||||||
|
} |
||||||
|
export async function publicSharedBaseGet(param: { |
||||||
|
sharedBaseUuid: string; |
||||||
|
}): Promise<any> { |
||||||
|
const project = await Project.getByUuid(param.sharedBaseUuid); |
||||||
|
|
||||||
|
if (!project) { |
||||||
|
NcError.notFound(); |
||||||
|
} |
||||||
|
|
||||||
|
return { project_id: project.id }; |
||||||
|
} |
Loading…
Reference in new issue