Browse Source

feat: public shared data apis (WIP)

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 1 year ago
parent
commit
f78d0df47b
  1. 3
      packages/nocodb-nest/src/app.module.ts
  2. 20
      packages/nocodb-nest/src/modules/public-datas/public-datas.controller.spec.ts
  3. 113
      packages/nocodb-nest/src/modules/public-datas/public-datas.controller.ts
  4. 9
      packages/nocodb-nest/src/modules/public-datas/public-datas.module.ts
  5. 18
      packages/nocodb-nest/src/modules/public-datas/public-datas.service.spec.ts
  6. 476
      packages/nocodb-nest/src/modules/public-datas/public-datas.service.ts

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

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

20
packages/nocodb-nest/src/modules/public-datas/public-datas.controller.spec.ts

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

113
packages/nocodb-nest/src/modules/public-datas/public-datas.controller.ts

@ -0,0 +1,113 @@
import { Controller, Get, Param, Post, Request } from '@nestjs/common';
import { PublicDatasService } from './public-datas.service';
@Controller('public-datas')
export class PublicDatasController {
constructor(private readonly publicDatasService: PublicDatasService) {}
@Get('/api/v1/db/public/shared-view/:sharedViewUuid/rows')
async dataList(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
) {
const pagedResponse = await this.publicDatasService.dataList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid,
});
return pagedResponse;
}
// todo: Handle the error case where view doesnt belong to model
@Get('/api/v1/db/public/shared-view/:sharedViewUuid/group/:columnId')
async groupedDataList(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
@Param('columnId') columnId: string,
) {
const groupedData = await this.publicDatasService.groupedDataList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid: sharedViewUuid,
groupColumnId: columnId,
});
return groupedData;
}
// todo: multer
// router.post(
// '/api/v1/db/public/shared-view/:sharedViewUuid/rows',
// multer({
// storage: multer.diskStorage({}),
// limits: {
// fieldSize: NC_ATTACHMENT_FIELD_SIZE,
// },
// }).any(),
// catchError(dataInsert)
// );
@Post('/api/v1/db/public/shared-view/:sharedViewUuid/rows')
async dataInsert(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
) {
const insertResult = await this.publicDatasService.dataInsert({
sharedViewUuid: sharedViewUuid,
password: req.headers?.['xc-password'] as string,
body: req.body?.data,
siteUrl: (req as any).ncSiteUrl,
files: req.files,
});
return insertResult;
}
@Get('/api/v1/db/public/shared-view/:sharedViewUuid/nested/:columnId')
async relDataList(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
@Param('columnId') columnId: string,
) {
const pagedResponse = await this.publicDatasService.relDataList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid: sharedViewUuid,
columnId: columnId,
});
return pagedResponse;
}
@Get('/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/mm/:colId')
async publicMmList(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
@Param('rowId') rowId: string,
@Param('colId') colId: string,
) {
const paginatedResponse = await this.publicDatasService.publicMmList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid: sharedViewUuid,
columnId: colId,
rowId: rowId,
});
return paginatedResponse;
}
@Get('/api/v1/db/public/shared-view/:sharedViewUuid/rows/:rowId/hm/:colId')
async publicHmList(
@Request() req,
@Param('sharedViewUuid') sharedViewUuid: string,
@Param('rowId') rowId: string,
@Param('colId') colId: string,
) {
const paginatedResponse = await this.publicDatasService.publicHmList({
query: req.query,
password: req.headers?.['xc-password'] as string,
sharedViewUuid: sharedViewUuid,
columnId: colId,
rowId: rowId,
});
return paginatedResponse;
}
}

9
packages/nocodb-nest/src/modules/public-datas/public-datas.module.ts

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PublicDatasService } from './public-datas.service';
import { PublicDatasController } from './public-datas.controller';
@Module({
controllers: [PublicDatasController],
providers: [PublicDatasService]
})
export class PublicDatasModule {}

18
packages/nocodb-nest/src/modules/public-datas/public-datas.service.spec.ts

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

476
packages/nocodb-nest/src/modules/public-datas/public-datas.service.ts

@ -0,0 +1,476 @@
import { Injectable } from '@nestjs/common';
import { nanoid } from 'nanoid';
import { ErrorMessages, UITypes, ViewTypes } from 'nocodb-sdk';
import path from 'path';
import slash from 'slash';
import {
Base,
Column,
LinkToAnotherRecordColumn,
Model,
View,
} from 'src/models';
import { sanitizeUrlPath } from '../../../../nocodb/src/lib/services/attachment.svc';
import { NcError } from '../../helpers/catchError';
import getAst from '../../helpers/getAst';
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { nocoExecute } from 'nc-help';
import { mimeIcons } from '../../utils/mimeTypes';
import { getColumnByIdOrName } from '../datas/helpers';
@Injectable()
export class PublicDatasService {
async 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: await 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 {
const { ast } = await getAst({
query: param.query,
model,
view,
});
data = await nocoExecute(
ast,
await baseModel.list(listArgs),
{},
listArgs,
);
count = await baseModel.count(listArgs);
} catch (e) {
console.log(e);
// show empty result instead of throwing error here
// e.g. search some text in a numeric field
NcError.internalServerError('Please try after some time');
}
return new PagedResponseImpl(data, { ...param.query, count });
}
// todo: Handle the error case where view doesnt belong to model
async 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 this.getGroupedDataList({
model,
view,
query: param.query,
groupColumnId: param.groupColumnId,
});
}
async 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: await NcConnectionMgrv2.get(base),
});
const { ast } = 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: ast },
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;
}
async 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: await 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);
}
async 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: await NcConnectionMgrv2.get(base),
});
const { ast } = await getAst({
query: param.query,
model,
extractOnlyPrimaries: true,
});
let data = [];
let count = 0;
try {
data = data = await nocoExecute(
ast,
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 });
}
async 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: await 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 });
}
async 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: await 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 });
}
}
Loading…
Cancel
Save