Browse Source

feat: fallback job queue

Signed-off-by: mertmit <mertmit99@gmail.com>
feat/export-nest-dir-restructure
mertmit 2 years ago committed by starbirdtech383
parent
commit
d390346135
  1. 3
      packages/nc-gui/pages/index/index/index.vue
  2. 12
      packages/nc-gui/plugins/events.ts
  3. 67
      packages/nocodb-nest/package-lock.json
  4. 1
      packages/nocodb-nest/package.json
  5. 13
      packages/nocodb-nest/src/app.module.ts
  6. 15
      packages/nocodb-nest/src/modules/jobs/export-import/duplicate.controller.ts
  7. 14
      packages/nocodb-nest/src/modules/jobs/export-import/duplicate.processor.ts
  8. 1
      packages/nocodb-nest/src/modules/jobs/export-import/export.service.ts
  9. 102
      packages/nocodb-nest/src/modules/jobs/fallback-queue.service.ts
  10. 29
      packages/nocodb-nest/src/modules/jobs/jobs.gateway.ts
  11. 4
      packages/nocodb-nest/src/modules/jobs/jobs.module.ts
  12. 31
      packages/nocodb-nest/src/modules/jobs/jobs.service.ts
  13. 4
      packages/nocodb-nest/src/schema/swagger.json
  14. 8
      packages/nocodb-sdk/src/lib/Api.ts

3
packages/nc-gui/pages/index/index/index.vue

@ -85,7 +85,8 @@ const duplicateProject = (project: ProjectType) => {
try { try {
const jobData = await api.project.duplicate(project.id as string) const jobData = await api.project.duplicate(project.id as string)
$events.subscribe(jobData.type, jobData.id, async (data: { status: string }) => { $events.subscribe(jobData.name, jobData.id, async (data: { status: string }) => {
console.log('dataCB', data)
if (data.status === 'completed' || data.status === 'refresh') { if (data.status === 'completed' || data.status === 'refresh') {
await loadProjects() await loadProjects()
} else if (data.status === 'failed') { } else if (data.status === 'failed') {

12
packages/nc-gui/plugins/events.ts

@ -29,11 +29,11 @@ export default defineNuxtPlugin(async (nuxtApp) => {
} }
const events = { const events = {
subscribe(type: string, id: string, cb: (data: any) => void) { subscribe(name: string, id: string, cb: (data: any) => void) {
if (socket) { if (socket) {
socket.emit('subscribe', { type, id }) socket.emit('subscribe', { name, id })
const tempFn = (data: any) => { const tempFn = (data: any) => {
if (data.id === id && data.type === type) { if (data.id === id && data.name === name) {
cb(data) cb(data)
if (data.status === 'completed' || data.status === 'failed') { if (data.status === 'completed' || data.status === 'failed') {
socket?.off('status', tempFn) socket?.off('status', tempFn)
@ -43,12 +43,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
socket.on('status', tempFn) socket.on('status', tempFn)
} }
}, },
getStatus(type: string, id: string): Promise<string> { getStatus(name: string, id: string): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (socket) { if (socket) {
socket.emit('status', { type, id }) socket.emit('status', { name, id })
const tempFn = (data: any) => { const tempFn = (data: any) => {
if (data.id === id && data.type === type) { if (data.id === id && data.name === name) {
resolve(data.status) resolve(data.status)
socket?.off('status', tempFn) socket?.off('status', tempFn)
} }

67
packages/nocodb-nest/package-lock.json generated

@ -86,6 +86,7 @@
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
"p-queue": "^6.6.2",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",
"parse-database-url": "^0.3.0", "parse-database-url": "^0.3.0",
"passport": "^0.4.1", "passport": "^0.4.1",
@ -21303,6 +21304,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/events": { "node_modules/events": {
"version": "3.3.0", "version": "3.3.0",
"license": "MIT", "license": "MIT",
@ -26088,6 +26094,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"engines": {
"node": ">=4"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"license": "MIT", "license": "MIT",
@ -26129,6 +26143,32 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": { "node_modules/p-try": {
"version": "2.2.0", "version": "2.2.0",
"dev": true, "dev": true,
@ -34239,6 +34279,11 @@
"event-target-shim": { "event-target-shim": {
"version": "5.0.1" "version": "5.0.1"
}, },
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"events": { "events": {
"version": "3.3.0" "version": "3.3.0"
}, },
@ -46431,6 +46476,11 @@
"minimist": "^1.1.0" "minimist": "^1.1.0"
} }
}, },
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="
},
"p-limit": { "p-limit": {
"version": "3.1.0", "version": "3.1.0",
"requires": { "requires": {
@ -46451,6 +46501,23 @@
"aggregate-error": "^3.0.0" "aggregate-error": "^3.0.0"
} }
}, },
"p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"requires": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
}
},
"p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"requires": {
"p-finally": "^1.0.0"
}
},
"p-try": { "p-try": {
"version": "2.2.0", "version": "2.2.0",
"dev": true "dev": true

1
packages/nocodb-nest/package.json

@ -117,6 +117,7 @@
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
"p-queue": "^6.6.2",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",
"parse-database-url": "^0.3.0", "parse-database-url": "^0.3.0",
"passport": "^0.4.1", "passport": "^0.4.1",

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

@ -35,12 +35,13 @@ import type {
MetasModule, MetasModule,
DatasModule, DatasModule,
JobsModule, JobsModule,
BullModule.forRoot({ ...(process.env['NC_REDIS_URL']
redis: { ? [
host: 'localhost', BullModule.forRoot({
port: 6379, redis: process.env.NC_REDIS_URL,
}, }),
}), ]
: []),
], ],
controllers: [], controllers: [],
providers: [ providers: [

15
packages/nocodb-nest/src/modules/jobs/export-import/duplicate.controller.ts

@ -14,13 +14,20 @@ import {
Acl, Acl,
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
} from 'src/middlewares/extract-project-id/extract-project-id.middleware'; } from 'src/middlewares/extract-project-id/extract-project-id.middleware';
import { QueueService } from '../fallback-queue.service';
@Controller() @Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard) @UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DuplicateController { export class DuplicateController {
activeQueue;
constructor( constructor(
@InjectQueue('duplicate') private readonly duplicateQueue: Queue, @InjectQueue('jobs') private readonly jobsQueue: Queue,
) {} private readonly fallbackQueueService: QueueService,
) {
this.activeQueue = process.env.NC_REDIS_URL
? this.jobsQueue
: this.fallbackQueueService;
}
@Post('/api/v1/db/meta/duplicate/:projectId/:baseId?') @Post('/api/v1/db/meta/duplicate/:projectId/:baseId?')
@HttpCode(200) @HttpCode(200)
@ -30,7 +37,7 @@ export class DuplicateController {
@Param('projectId') projectId: string, @Param('projectId') projectId: string,
@Param('baseId') baseId?: string, @Param('baseId') baseId?: string,
) { ) {
const job = await this.duplicateQueue.add('duplicate', { const job = await this.activeQueue.add('duplicate', {
projectId, projectId,
baseId, baseId,
req: { req: {
@ -38,6 +45,6 @@ export class DuplicateController {
clientIp: req.clientIp, clientIp: req.clientIp,
}, },
}); });
return { id: job.id, type: job.name }; return { id: job.id, name: job.name };
} }
} }

14
packages/nocodb-nest/src/modules/jobs/export-import/duplicate.processor.ts

@ -17,6 +17,7 @@ import {
} from 'src/helpers/exportImportHelpers'; } from 'src/helpers/exportImportHelpers';
import { BulkDataAliasService } from 'src/services/bulk-data-alias.service'; import { BulkDataAliasService } from 'src/services/bulk-data-alias.service';
import { UITypes } from 'nocodb-sdk'; import { UITypes } from 'nocodb-sdk';
import { forwardRef, Inject } from '@nestjs/common';
import { JobsGateway } from '../jobs.gateway'; import { JobsGateway } from '../jobs.gateway';
import { ExportService } from './export.service'; import { ExportService } from './export.service';
import { ImportService } from './import.service'; import { ImportService } from './import.service';
@ -24,20 +25,21 @@ import type { LinkToAnotherRecordColumn } from 'src/models';
const DEBUG = false; const DEBUG = false;
@Processor('duplicate') @Processor('jobs')
export class DuplicateProcessor { export class DuplicateProcessor {
constructor( constructor(
private readonly exportService: ExportService, private readonly exportService: ExportService,
private readonly importService: ImportService, private readonly importService: ImportService,
private readonly projectsService: ProjectsService, private readonly projectsService: ProjectsService,
private readonly bulkDataService: BulkDataAliasService, private readonly bulkDataService: BulkDataAliasService,
@Inject(forwardRef(() => JobsGateway))
private readonly jobsGateway: JobsGateway, private readonly jobsGateway: JobsGateway,
) {} ) {}
@OnQueueActive() @OnQueueActive()
onActive(job: Job) { onActive(job: Job) {
this.jobsGateway.jobStatus({ this.jobsGateway.jobStatus({
type: job.name, name: job.name,
id: job.id.toString(), id: job.id.toString(),
status: 'active', status: 'active',
}); });
@ -47,7 +49,7 @@ export class DuplicateProcessor {
onFailed(job: Job, error: Error) { onFailed(job: Job, error: Error) {
console.error( console.error(
boxen( boxen(
`---- !! JOB FAILED !! ----\ntype: ${job.name}\nid:${job.id}\nerror:${error.name} (${error.message})\n\nstack: ${error.stack}`, `---- !! JOB FAILED !! ----\nname: ${job.name}\nid:${job.id}\nerror:${error.name} (${error.message})\n\nstack: ${error.stack}`,
{ {
padding: 1, padding: 1,
borderStyle: 'double', borderStyle: 'double',
@ -57,7 +59,7 @@ export class DuplicateProcessor {
); );
this.jobsGateway.jobStatus({ this.jobsGateway.jobStatus({
type: job.name, name: job.name,
id: job.id.toString(), id: job.id.toString(),
status: 'failed', status: 'failed',
}); });
@ -66,7 +68,7 @@ export class DuplicateProcessor {
@OnQueueCompleted() @OnQueueCompleted()
onCompleted(job: Job) { onCompleted(job: Job) {
this.jobsGateway.jobStatus({ this.jobsGateway.jobStatus({
type: job.name, name: job.name,
id: job.id.toString(), id: job.id.toString(),
status: 'completed', status: 'completed',
}); });
@ -134,7 +136,7 @@ export class DuplicateProcessor {
}); });
this.jobsGateway.jobStatus({ this.jobsGateway.jobStatus({
type: job.name, name: job.name,
id: job.id.toString(), id: job.id.toString(),
status: 'refresh', status: 'refresh',
}); });

1
packages/nocodb-nest/src/modules/jobs/export-import/export.service.ts

@ -12,7 +12,6 @@ import { Base, Model, Project } from 'src/models';
import { DatasService } from 'src/services/datas.service'; import { DatasService } from 'src/services/datas.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import type { LinkToAnotherRecordColumn, View } from 'src/models'; import type { LinkToAnotherRecordColumn, View } from 'src/models';
import type { IStorageAdapterV2 } from 'nc-plugin';
@Injectable() @Injectable()
export class ExportService { export class ExportService {

102
packages/nocodb-nest/src/modules/jobs/fallback-queue.service.ts

@ -0,0 +1,102 @@
import { Injectable } from '@nestjs/common';
import PQueue from 'p-queue';
import Emittery from 'emittery';
import { DuplicateProcessor } from './export-import/duplicate.processor';
interface Job {
id: string;
name: string;
status: string;
data: any;
}
@Injectable()
export class QueueService {
static queue = new PQueue({ concurrency: 1 });
static queueIndex = 1;
static processed = 0;
static queueMemory: Job[] = [];
static emitter = new Emittery();
constructor(private readonly duplicateProcessor: DuplicateProcessor) {
this.emitter.on('active', (data: any) => {
const job = this.queueMemory.find(
(job) => job.id === data.id && job.name === data.name,
);
job.status = 'active';
this.duplicateProcessor.onActive.apply(this.duplicateProcessor, [
job as any,
]);
});
this.emitter.on('completed', (data: any) => {
const job = this.queueMemory.find(
(job) => job.id === data.id && job.name === data.name,
);
job.status = 'completed';
this.duplicateProcessor.onCompleted.apply(this.duplicateProcessor, [
data as any,
]);
});
this.emitter.on('failed', (data: { job: Job; error: Error }) => {
const job = this.queueMemory.find(
(job) => job.id === data.job.id && job.name === data.job.name,
);
job.status = 'failed';
this.duplicateProcessor.onFailed.apply(this.duplicateProcessor, [
data.job as any,
data.error,
]);
});
}
jobMap = {
duplicate: this.duplicateProcessor.duplicateBase,
};
async jobWrapper(job: Job) {
this.emitter.emit('active', job);
try {
await this.jobMap[job.name].apply(this.duplicateProcessor, [job]);
this.emitter.emit('completed', job);
} catch (error) {
this.emitter.emit('failed', { job, error });
}
}
get emitter() {
return QueueService.emitter;
}
get queue() {
return QueueService.queue;
}
get queueMemory() {
return QueueService.queueMemory;
}
get queueIndex() {
return QueueService.queueIndex;
}
set queueIndex(index: number) {
QueueService.queueIndex = index;
}
async add(name: string, data: any) {
const id = `${this.queueIndex++}`;
const job = { id: `${id}`, name, status: 'waiting', data };
this.queueMemory.push(job);
this.queue.add(() => this.jobWrapper(job));
return { id, name };
}
async getJobs(types: string[] | string) {
types = Array.isArray(types) ? types : [types];
return this.queueMemory.filter((q) => types.includes(q.status));
}
async getJob(id: string) {
return this.queueMemory.find((q) => q.id === id);
}
}

29
packages/nocodb-nest/src/modules/jobs/jobs.gateway.ts

@ -6,7 +6,7 @@ import {
WebSocketServer, WebSocketServer,
} from '@nestjs/websockets'; } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { JobsService } from './jobs.service'; import { JobsService } from './jobs.service';
@ -22,7 +22,10 @@ import type { OnModuleInit } from '@nestjs/common';
}) })
@Injectable() @Injectable()
export class JobsGateway implements OnModuleInit { export class JobsGateway implements OnModuleInit {
constructor(private readonly jobsService: JobsService) {} constructor(
@Inject(forwardRef(() => JobsService))
private readonly jobsService: JobsService,
) {}
@WebSocketServer() @WebSocketServer()
server: Server; server: Server;
@ -41,30 +44,32 @@ export class JobsGateway implements OnModuleInit {
@SubscribeMessage('subscribe') @SubscribeMessage('subscribe')
async subscribe( async subscribe(
@MessageBody() data: { type: string; id: string }, @MessageBody() data: { name: string; id: string },
@ConnectedSocket() client: Socket, @ConnectedSocket() client: Socket,
): Promise<void> { ): Promise<void> {
const rooms = await this.jobsService.jobList(data.type); const rooms = (await this.jobsService.jobList(data.name)).map(
const room = rooms.find((r) => r.id === data.id); (j) => `${j.name}-${j.id}`,
);
const room = rooms.find((r) => r === `${data.name}-${data.id}`);
if (room) { if (room) {
client.join(data.id); client.join(`${data.name}-${data.id}`);
} }
} }
@SubscribeMessage('status') @SubscribeMessage('status')
async status( async status(
@MessageBody() data: { type: string; id: string }, @MessageBody() data: { name: string; id: string },
@ConnectedSocket() client: Socket, @ConnectedSocket() client: Socket,
): Promise<void> { ): Promise<void> {
client.emit('status', { client.emit('status', {
id: data.id, id: data.id,
type: data.type, name: data.name,
status: await this.jobsService.jobStatus(data.type, data.id), status: await this.jobsService.jobStatus(data.id),
}); });
} }
async jobStatus(data: { async jobStatus(data: {
type: string; name: string;
id: string; id: string;
status: status:
| 'completed' | 'completed'
@ -75,9 +80,9 @@ export class JobsGateway implements OnModuleInit {
| 'paused' | 'paused'
| 'refresh'; | 'refresh';
}): Promise<void> { }): Promise<void> {
this.server.to(data.id).emit('status', { this.server.to(`${data.name}-${data.id}`).emit('status', {
id: data.id, id: data.id,
type: data.type, name: data.name,
status: data.status, status: data.status,
}); });
} }

4
packages/nocodb-nest/src/modules/jobs/jobs.module.ts

@ -9,6 +9,7 @@ import { ImportService } from './export-import/import.service';
import { DuplicateController } from './export-import/duplicate.controller'; import { DuplicateController } from './export-import/duplicate.controller';
import { DuplicateProcessor } from './export-import/duplicate.processor'; import { DuplicateProcessor } from './export-import/duplicate.processor';
import { JobsGateway } from './jobs.gateway'; import { JobsGateway } from './jobs.gateway';
import { QueueService } from './fallback-queue.service';
@Module({ @Module({
imports: [ imports: [
@ -16,11 +17,12 @@ import { JobsGateway } from './jobs.gateway';
DatasModule, DatasModule,
MetasModule, MetasModule,
BullModule.registerQueue({ BullModule.registerQueue({
name: 'duplicate', name: 'jobs',
}), }),
], ],
controllers: [DuplicateController], controllers: [DuplicateController],
providers: [ providers: [
QueueService,
JobsGateway, JobsGateway,
JobsService, JobsService,
DuplicateProcessor, DuplicateProcessor,

31
packages/nocodb-nest/src/modules/jobs/jobs.service.ts

@ -1,28 +1,27 @@
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Queue } from 'bull'; import { Queue } from 'bull';
import { QueueService } from './fallback-queue.service';
@Injectable() @Injectable()
export class JobsService { export class JobsService {
constructor(@InjectQueue('duplicate') private duplicateQueue: Queue) {} activeQueue;
constructor(
@InjectQueue('jobs') private readonly jobsQueue: Queue,
private readonly fallbackQueueService: QueueService,
) {
this.activeQueue = process.env.NC_REDIS_URL
? this.jobsQueue
: this.fallbackQueueService;
}
async jobStatus(jobType: string, jobId: string) { async jobStatus(jobId: string) {
switch (jobType) { return await (await this.activeQueue.getJob(jobId)).getState();
case 'duplicate':
default:
return await (await this.duplicateQueue.getJob(jobId)).getState();
}
} }
async jobList(jobType: string) { async jobList(jobType: string) {
switch (jobType) { return (
case 'duplicate': await this.activeQueue.getJobs(['active', 'waiting', 'delayed'])
default: ).filter((j) => j.name === jobType);
return await this.duplicateQueue.getJobs([
'active',
'waiting',
'delayed',
]);
}
} }
} }

4
packages/nocodb-nest/src/schema/swagger.json

@ -2112,7 +2112,7 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "name": {
"type": "string" "type": "string"
}, },
"id": { "id": {
@ -2170,7 +2170,7 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "name": {
"type": "string" "type": "string"
}, },
"id": { "id": {

8
packages/nocodb-sdk/src/lib/Api.ts

@ -4019,7 +4019,7 @@ export class Api<
* @summary Duplicate Project Base * @summary Duplicate Project Base
* @request POST:/api/v1/db/meta/duplicate/{projectId}/{baseId} * @request POST:/api/v1/db/meta/duplicate/{projectId}/{baseId}
* @response `200` `{ * @response `200` `{
type?: string, name?: string,
id?: string, id?: string,
}` OK }` OK
@ -4036,7 +4036,7 @@ export class Api<
) => ) =>
this.request< this.request<
{ {
type?: string; name?: string;
id?: string; id?: string;
}, },
{ {
@ -4058,7 +4058,7 @@ export class Api<
* @summary Duplicate Project * @summary Duplicate Project
* @request POST:/api/v1/db/meta/duplicate/{projectId} * @request POST:/api/v1/db/meta/duplicate/{projectId}
* @response `200` `{ * @response `200` `{
type?: string, name?: string,
id?: string, id?: string,
}` OK }` OK
@ -4071,7 +4071,7 @@ export class Api<
duplicate: (projectId: IdType, params: RequestParams = {}) => duplicate: (projectId: IdType, params: RequestParams = {}) =>
this.request< this.request<
{ {
type?: string; name?: string;
id?: string; id?: string;
}, },
{ {

Loading…
Cancel
Save