Skip to content

Commit

Permalink
feat: credit check
Browse files Browse the repository at this point in the history
  • Loading branch information
tea-artist committed Mar 13, 2024
1 parent c4744ce commit 6f33774
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 78 deletions.
1 change: 1 addition & 0 deletions apps/nestjs-backend/src/configs/threshold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const thresholdConfig = registerAs('threshold', () => ({
maxSyncUpdateCells: Number(process.env.MAX_SYNC_UPDATE_CELLS ?? 10_000),
maxGroupPoints: Number(process.env.MAX_GROUP_POINTS ?? 5_000),
calcChunkSize: Number(process.env.CALC_CHUNK_SIZE ?? 1_000),
maxFreeRowLimit: Number(process.env.MAX_FREE_ROW_LIMIT ?? 0),
estimateCalcCelPerMs: Number(process.env.ESTIMATE_CALC_CEL_PER_MS ?? 3),
bigTransactionTimeout: Number(
process.env.BIG_TRANSACTION_TIMEOUT ?? 10 * 60 * 1000 /* 10 mins */
Expand Down
103 changes: 29 additions & 74 deletions apps/nestjs-backend/src/features/record/record.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { Knex } from 'knex';
import { keyBy } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { ClsService } from 'nestjs-cls';
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
import { InjectDbProvider } from '../../db-provider/db.provider';
import { IDbProvider } from '../../db-provider/db.provider.interface';
import type { IAdapterService } from '../../share-db/interface';
Expand Down Expand Up @@ -69,8 +70,9 @@ export class RecordService implements IAdapterService {
private readonly batchService: BatchService,
private readonly attachmentStorageService: AttachmentsStorageService,
private readonly cls: ClsService<IClsStore>,
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
@InjectDbProvider() private readonly dbProvider: IDbProvider,
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex
@ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
) {}

private async getRowOrderFieldNames(tableId: string) {
Expand All @@ -88,34 +90,6 @@ export class RecordService implements IAdapterService {
return views.map((view) => `${ROW_ORDER_FIELD_PREFIX}_${view.id}`);
}

// get fields create by users
private async getUserFields(tableId: string, createRecordsRo: ICreateRecordsRo) {
const fieldIdSet = createRecordsRo.records.reduce<Set<string>>((acc, record) => {
const fieldIds = Object.keys(record.fields);
fieldIds.forEach((fieldId) => acc.add(fieldId));
return acc;
}, new Set());

const userFieldIds = Array.from(fieldIdSet);

const userFields = await this.prismaService.txClient().field.findMany({
where: {
tableId,
id: { in: userFieldIds },
},
select: {
id: true,
dbFieldName: true,
},
});

if (userFields.length !== userFieldIds.length) {
throw new BadRequestException('some fields not found');
}

return userFields;
}

private dbRecord2RecordFields(
record: IRecord['fields'],
fields: IFieldInstance[],
Expand Down Expand Up @@ -173,51 +147,6 @@ export class RecordService implements IAdapterService {
return dbValueMatrix;
}

async multipleCreateRecordTransaction(tableId: string, createRecordsRo: ICreateRecordsRo) {
const { dbTableName } = await this.prismaService.txClient().tableMeta.findUniqueOrThrow({
where: {
id: tableId,
},
select: {
dbTableName: true,
},
});

const userFields = await this.getUserFields(tableId, createRecordsRo);
const rowOrderFieldNames = await this.getRowOrderFieldNames(tableId);

const allDbFieldNames = [
...userFields.map((field) => field.dbFieldName),
...rowOrderFieldNames,
...['__id', '__created_time', '__created_by', '__version'],
];

const dbValueMatrix = await this.getDbValueMatrix(
dbTableName,
userFields,
rowOrderFieldNames,
createRecordsRo
);

const dbFieldSQL = allDbFieldNames.join(', ');
const dbValuesSQL = dbValueMatrix
.map((dbValues) => `(${dbValues.map((value) => JSON.stringify(value)).join(', ')})`)
.join(',\n');

return await this.prismaService.txClient().$executeRawUnsafe(`
INSERT INTO ${dbTableName} (${dbFieldSQL})
VALUES
${dbValuesSQL};
`);
}

// we have to support multiple action, because users will do it in batch
async multipleCreateRecords(tableId: string, createRecordsRo: ICreateRecordsRo) {
return await this.prismaService.$tx(async () => {
return this.multipleCreateRecordTransaction(tableId, createRecordsRo);
});
}

async getDbTableName(tableId: string) {
const tableMeta = await this.prismaService
.txClient()
Expand Down Expand Up @@ -636,8 +565,34 @@ export class RecordService implements IAdapterService {
await this.createBatch(tableId, [snapshot]);
}

async creditCheck(tableId: string) {
if (!this.thresholdConfig.maxFreeRowLimit) {
return;
}

const table = await this.prismaService.txClient().tableMeta.findFirstOrThrow({
where: { id: tableId, deletedTime: null },
select: { dbTableName: true, base: { select: { space: { select: { credit: true } } } } },
});

const rowCount = await this.getAllRecordCount(table.dbTableName);

const maxRowCount =
table.base.space.credit == null
? this.thresholdConfig.maxFreeRowLimit
: table.base.space.credit;

if (rowCount >= maxRowCount) {
this.logger.log(`Exceed row count: ${maxRowCount}`, 'creditCheck');
throw new BadRequestException(
`Exceed max row limit: ${maxRowCount}, please contact us to increase the limit`
);
}
}

private async createBatch(tableId: string, records: IRecord[]) {
const userId = this.cls.get('user.id');
await this.creditCheck(tableId);
const dbTableName = await this.getDbTableName(tableId);

const maxRecordOrder = await this.getMaxRecordOrder(dbTableName);
Expand Down
50 changes: 50 additions & 0 deletions apps/nestjs-backend/test/credit.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable sonarjs/no-duplicate-string */
import type { INestApplication } from '@nestjs/common';
import type { ITableFullVo } from '@teable/core';
import { FieldKeyType } from '@teable/core';
import { createRecords, createTable, deleteTable, initApp } from './utils/init-app';

describe('Credit limit (e2e)', () => {
let app: INestApplication;
const baseId = globalThis.testConfig.baseId;

beforeAll(async () => {
process.env.MAX_FREE_ROW_LIMIT = '10';
const appCtx = await initApp();
app = appCtx.app;
});

afterAll(async () => {
process.env.MAX_FREE_ROW_LIMIT = undefined;
await app.close();
});

describe('max row limit', () => {
let table: ITableFullVo;
beforeEach(async () => {
table = await createTable(baseId, { name: 'table1' });
});

afterEach(async () => {
await deleteTable(baseId, table.id);
});

it('should create a record', async () => {
// create 6 record succeed, 3(default) + 7 = 10
await createRecords(table.id, {
fieldKeyType: FieldKeyType.Name,
records: Array.from({ length: 7 }).map(() => ({ fields: {} })),
});

// limit exceed
await createRecords(
table.id,
{
fieldKeyType: FieldKeyType.Name,
records: [{ fields: {} }],
},
400
);
});
});
});
10 changes: 6 additions & 4 deletions apps/nestjs-backend/test/table.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,13 @@ describe('OpenAPI TableController (e2e)', () => {
});

afterAll(async () => {
await deleteTable(baseId, tableId);

await app.close();
});

afterEach(async () => {
await deleteTable(baseId, tableId);
});

it('/api/table/ (POST) with assertData data', async () => {
let eventCount = 0;
event.once(Events.TABLE_CREATE, async (payload: TableCreateEvent) => {
Expand Down Expand Up @@ -228,14 +230,14 @@ describe('OpenAPI TableController (e2e)', () => {
await updateTableName(baseId, tableId, { name: 'newTableName' });
await updateTableDescription(baseId, tableId, { description: 'newDescription' });
await updateTableIcon(baseId, tableId, { icon: '😀' });
await updateTableOrder(baseId, tableId, { order: 1.11 });
await updateTableOrder(baseId, tableId, { order: 1.5 });

const table = await getTable(baseId, tableId);

expect(table.name).toEqual('newTableName');
expect(table.description).toEqual('newDescription');
expect(table.icon).toEqual('😀');
expect(table.order).toEqual(1.11);
expect(table.order).toEqual(1.5);
});

it('should delete table and clean up link and lookup fields', async () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/nextjs-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ ESTIMATE_CALC_CEL_PER_MS=3
BIG_TRANSACTION_TIMEOUT=600000
# the maximum number of base db connections a user can make
DEFAULT_MAX_BASE_DB_CONNECTIONS=3
# the maxium row limit when space has no credit, ingore it when you don't want to limit it
MAX_FREE_ROW_LIMIT=100000

# your redis cache connection uri
BACKEND_CACHE_PROVIDER=redis
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "space" ADD COLUMN "credit" INTEGER;
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/postgres/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ datasource db {
model Space {
id String @id @default(cuid())
name String
credit Int?
deletedTime DateTime? @map("deleted_time")
createdTime DateTime @default(now()) @map("created_time")
createdBy String @map("created_by")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "space" ADD COLUMN "credit" INTEGER;
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/sqlite/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ datasource db {
model Space {
id String @id @default(cuid())
name String
credit Int?
deletedTime DateTime? @map("deleted_time")
createdTime DateTime @default(now()) @map("created_time")
createdBy String @map("created_by")
Expand Down
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/template.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ datasource db {
model Space {
id String @id @default(cuid())
name String
credit Int?
deletedTime DateTime? @map("deleted_time")
createdTime DateTime @default(now()) @map("created_time")
createdBy String @map("created_by")
Expand Down

0 comments on commit 6f33774

Please sign in to comment.