Skip to content

Commit ecc2d25

Browse files
[sync] [T2449] support native v2 bulk record updates (#1494) (#2814)
Synced from teableio/teable-ee@3605235 Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent cf35408 commit ecc2d25

30 files changed

Lines changed: 2638 additions & 472 deletions

apps/nestjs-backend/src/cache/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ICacheStore {
2323
// userId:tableId:windowId
2424
[key: `operations:undo:${string}:${string}:${string}`]: IUndoRedoOperation[];
2525
[key: `operations:redo:${string}:${string}:${string}`]: IUndoRedoOperation[];
26+
[key: `operations:engine:${string}:${string}:${string}`]: 'v1' | 'v2';
2627
[key: `plugin:auth-code:${string}`]: IPluginAuthStore;
2728
[key: `signin:attempts:${string}`]: number;
2829
[key: `signin:lockout:${string}`]: boolean;

apps/nestjs-backend/src/features/record/open-api/record-open-api-v2.service.spec.ts

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { CellValueType, FieldType, SortFunc, TimeFormatting } from '@teable/core';
1+
import { CellValueType, FieldKeyType, FieldType, SortFunc, TimeFormatting } from '@teable/core';
22
import {
3-
FieldKeyType,
43
ListTableRecordsQuery,
54
ListTableRecordsResult,
5+
UpdateRecordsResult,
66
v2CoreTokens,
77
} from '@teable/v2-core';
88
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -17,8 +17,12 @@ describe('RecordOpenApiV2Service', () => {
1717
const getReadQuerySource = vi.fn();
1818
const getFieldsByQuery = vi.fn();
1919
const execute = vi.fn();
20+
const commandExecute = vi.fn();
2021
const resolve = vi.fn();
2122
const getContainer = vi.fn();
23+
const clsGet = vi.fn();
24+
const cacheDel = vi.fn();
25+
const cacheSetDetail = vi.fn();
2226

2327
let service: RecordOpenApiV2Service;
2428

@@ -29,12 +33,28 @@ describe('RecordOpenApiV2Service', () => {
2933
if (token === v2CoreTokens.queryBus) {
3034
return { execute };
3135
}
36+
if (token === v2CoreTokens.commandBus) {
37+
return { execute: commandExecute };
38+
}
3239
return undefined;
3340
});
3441
getContainer.mockResolvedValue({ resolve });
3542
createContext.mockResolvedValue({});
43+
clsGet.mockImplementation((key: string) => {
44+
if (key === 'user.id') {
45+
return `usr${'h'.repeat(16)}`;
46+
}
47+
if (key === 'windowId') {
48+
return `win${'i'.repeat(16)}`;
49+
}
50+
return undefined;
51+
});
3652
getReadQuerySource.mockResolvedValue(undefined);
3753
getFieldsByQuery.mockResolvedValue([]);
54+
commandExecute.mockResolvedValue({
55+
isErr: () => false,
56+
value: UpdateRecordsResult.create(2, []),
57+
});
3858
execute.mockResolvedValue({
3959
isErr: () => false,
4060
value: ListTableRecordsResult.create(
@@ -51,14 +71,13 @@ describe('RecordOpenApiV2Service', () => {
5171
{ data: { id: 'rec1111111111111111', fields: {} } },
5272
{ data: { id: 'rec2222222222222222', fields: {} } },
5373
]);
54-
5574
service = new RecordOpenApiV2Service(
5675
{ getContainer } as never,
5776
{ createContext } as never,
5877
{ getDocIdsByQuery, getSnapshotBulkWithPermission } as never,
5978
{} as never,
60-
{} as never,
61-
{ get: vi.fn() } as never,
79+
{ get: clsGet } as never,
80+
{ del: cacheDel, setDetail: cacheSetDetail } as never,
6281
{ getFieldsByQuery } as never,
6382
{ getReadQuerySource } as never,
6483
{} as never,
@@ -263,4 +282,93 @@ describe('RecordOpenApiV2Service', () => {
263282
},
264283
]);
265284
});
285+
286+
it('routes explicit batch field updates through native v2 updateRecords', async () => {
287+
getSnapshotBulkWithPermission.mockResolvedValue([
288+
{ data: { id: 'rec1111111111111111', fields: { fldStatus: 'Done' } } },
289+
{ data: { id: 'rec2222222222222222', fields: { fldStatus: 'Open' } } },
290+
]);
291+
292+
const result = await service.updateRecords(`tbl${'c'.repeat(16)}`, {
293+
fieldKeyType: FieldKeyType.Id,
294+
records: [
295+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Done' } },
296+
{ id: 'rec2222222222222222', fields: { fldStatus: 'Open' } },
297+
],
298+
});
299+
300+
expect(commandExecute).toHaveBeenCalledTimes(1);
301+
expect(commandExecute.mock.calls[0]?.[1].records).toHaveLength(2);
302+
expect(commandExecute.mock.calls[0]?.[1].records?.[0]?.recordId.toString()).toBe(
303+
'rec1111111111111111'
304+
);
305+
expect(commandExecute.mock.calls[0]?.[1].records?.[1]?.fieldValues.get('fldStatus')).toBe(
306+
'Open'
307+
);
308+
expect(commandExecute.mock.calls[0]?.[1].order).toBeUndefined();
309+
expect(result).toEqual([
310+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Done' } },
311+
{ id: 'rec2222222222222222', fields: { fldStatus: 'Open' } },
312+
]);
313+
expect(cacheDel).toHaveBeenCalledWith(
314+
`operations:engine:usr${'h'.repeat(16)}:tbl${'c'.repeat(16)}:win${'i'.repeat(16)}`
315+
);
316+
});
317+
318+
it('passes batch order through native v2 updateRecords', async () => {
319+
await service.updateRecords(`tbl${'c'.repeat(16)}`, {
320+
fieldKeyType: FieldKeyType.Id,
321+
records: [
322+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Done' } },
323+
{ id: 'rec2222222222222222', fields: { fldStatus: 'Open' } },
324+
],
325+
order: {
326+
viewId: `viw${'c'.repeat(16)}`,
327+
anchorId: 'rec1111111111111111',
328+
position: 'after',
329+
},
330+
});
331+
332+
expect(commandExecute).toHaveBeenCalledTimes(1);
333+
expect(commandExecute.mock.calls[0]?.[1].order?.viewId.toString()).toBe(`viw${'c'.repeat(16)}`);
334+
expect(commandExecute.mock.calls[0]?.[1].order?.position).toBe('after');
335+
});
336+
337+
it('merges duplicate record updates before calling native v2 updateRecords', async () => {
338+
getSnapshotBulkWithPermission.mockResolvedValue([
339+
{ data: { id: 'rec1111111111111111', fields: { fldStatus: 'Done', fldNote: 'latest' } } },
340+
]);
341+
342+
const result = await service.updateRecords(`tbl${'c'.repeat(16)}`, {
343+
fieldKeyType: FieldKeyType.Id,
344+
records: [
345+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Open', fldNote: 'first' } },
346+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Done' } },
347+
{ id: 'rec1111111111111111', fields: { fldNote: 'latest' } },
348+
],
349+
});
350+
351+
expect(commandExecute).toHaveBeenCalledTimes(1);
352+
expect(commandExecute.mock.calls[0]?.[1].records).toHaveLength(1);
353+
expect(commandExecute.mock.calls[0]?.[1].records?.[0]?.recordId.toString()).toBe(
354+
'rec1111111111111111'
355+
);
356+
expect(commandExecute.mock.calls[0]?.[1].records?.[0]?.fieldValues.get('fldStatus')).toBe(
357+
'Done'
358+
);
359+
expect(commandExecute.mock.calls[0]?.[1].records?.[0]?.fieldValues.get('fldNote')).toBe(
360+
'latest'
361+
);
362+
expect(getSnapshotBulkWithPermission).toHaveBeenCalledWith(
363+
`tbl${'c'.repeat(16)}`,
364+
['rec1111111111111111'],
365+
undefined,
366+
FieldKeyType.Id,
367+
undefined,
368+
true
369+
);
370+
expect(result).toEqual([
371+
{ id: 'rec1111111111111111', fields: { fldStatus: 'Done', fldNote: 'latest' } },
372+
]);
373+
});
266374
});

0 commit comments

Comments
 (0)