refactor: 将Data到ViewModel的转换由App转移至Server端
All checks were successful
deploy to server / build-and-deploy (push) Successful in 3m15s
All checks were successful
deploy to server / build-and-deploy (push) Successful in 3m15s
- 将逻辑转移到Server端后,简化前端逻辑
This commit is contained in:
66
server/utils/object.test.ts
Normal file
66
server/utils/object.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { expect, test, describe } from 'vitest';
|
||||
|
||||
/**
|
||||
* 单元测试: isObject
|
||||
*/
|
||||
describe('isObject', () => {
|
||||
test('identify plain object', () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ key: 'value' })).toBe(true);
|
||||
});
|
||||
test('identify null as non objevt', () => {
|
||||
expect(isObject(null)).toBe(false);
|
||||
});
|
||||
test('identify non-object types', () => {
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
expect(isObject(42)).toBe(false);
|
||||
expect(isObject('string')).toBe(false);
|
||||
expect(isObject(Symbol('sym'))).toBe(false);
|
||||
expect(isObject(true)).toBe(false);
|
||||
expect(isObject(() => {})).toBe(false);
|
||||
});
|
||||
test('identify arrays as objects', () => {
|
||||
expect(isObject([])).toBe(true);
|
||||
});
|
||||
test('identify narrowed object type', () => {
|
||||
const value: unknown = { id: 1 };
|
||||
if (isObject<{ id: number }>(value)) {
|
||||
expect(value.id).toBe(1);
|
||||
} else {
|
||||
throw new Error('Type narrowing failed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 单元测试: isArrayOfObject
|
||||
*/
|
||||
describe('isArrayOfObject', () => {
|
||||
test('identify array of plain objects', () => {
|
||||
const arr = [{ id: 1 }, { name: 'Alice' }];
|
||||
expect(isArrayOfObject<{ id?: number; name?: string }>(arr)).toBe(true);
|
||||
});
|
||||
test('identify array containing non-objects', () => {
|
||||
expect(isArrayOfObject([1, 2, 3])).toBe(false);
|
||||
expect(isArrayOfObject([{ id: 1 }, null])).toBe(false);
|
||||
expect(isArrayOfObject([{ id: 1 }, 'string'])).toBe(false);
|
||||
});
|
||||
test('identify non-array types', () => {
|
||||
expect(isArrayOfObject(null)).toBe(false);
|
||||
expect(isArrayOfObject({})).toBe(false);
|
||||
expect(isArrayOfObject(42)).toBe(false);
|
||||
});
|
||||
test('identify empty array as array of objects', () => {
|
||||
expect(isArrayOfObject([])).toBe(true);
|
||||
});
|
||||
test('identify narrowed array of object type', () => {
|
||||
const data: unknown = [{ id: 1 }, { id: 2 }];
|
||||
if (isArrayOfObject<{ id: number }>(data)) {
|
||||
// TS 能识别为 { id: number }[]
|
||||
expect(data[0].id).toBe(1);
|
||||
expect(data[1].id).toBe(2);
|
||||
} else {
|
||||
throw new Error('Type guard failed');
|
||||
}
|
||||
});
|
||||
});
|
||||
30
server/utils/object.ts
Normal file
30
server/utils/object.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 判断某一值是否为非null对象
|
||||
*
|
||||
* @template T 泛型类型,用于推断目标对象的类型
|
||||
* @param value: 需要判断的值
|
||||
* @returns 如果值是非null对象则返回true,否则返回false
|
||||
*
|
||||
* @example
|
||||
* if (isObject<Product>(value)) value.id
|
||||
*/
|
||||
export const isObject = <T extends object>(value: unknown): value is T =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
/**
|
||||
* 判断某一值是否为非null对象组成的数组
|
||||
*
|
||||
* @template T 泛型类型,用于推断目标对象的类型
|
||||
* @param value: 需要判断的值
|
||||
* @returns 如果值是非null对象组成的数组则返回true,否则返回false
|
||||
*
|
||||
* @example
|
||||
* const data: unknown = [{ id: 1 }, { id: 2 }];
|
||||
* if (isArrayOfObject)<{ id: number }>(data) {
|
||||
* // TypeScript 知道 data 是 { id: number }[] 类型
|
||||
* console.log(data[0].id);
|
||||
* }
|
||||
*/
|
||||
export const isArrayOfObject = <T extends object>(arr: unknown): arr is T[] => {
|
||||
return Array.isArray(arr) && arr.every(isObject<T>);
|
||||
};
|
||||
76
server/utils/search-converter.test.ts
Normal file
76
server/utils/search-converter.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
/**
|
||||
* 单元测试: converters
|
||||
*/
|
||||
describe('converters', () => {
|
||||
test('convert product item', () => {
|
||||
const item = {
|
||||
id: 1,
|
||||
name: 'Hydraulic Pump',
|
||||
summary: 'High efficiency',
|
||||
description: 'Detailed description',
|
||||
type: 'pump',
|
||||
};
|
||||
const result = converters.products(item);
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
type: 'product',
|
||||
title: 'Hydraulic Pump',
|
||||
summary: 'High efficiency',
|
||||
});
|
||||
});
|
||||
|
||||
test('convert solution item', () => {
|
||||
const item = {
|
||||
id: 1,
|
||||
title: 'Solution A',
|
||||
summary: 'Effective solution',
|
||||
content: 'Detailed content',
|
||||
type: 'Type A',
|
||||
};
|
||||
const result = converters.solutions(item);
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
type: 'solution',
|
||||
title: 'Solution A',
|
||||
summary: 'Effective solution',
|
||||
});
|
||||
});
|
||||
|
||||
test('convert question item', () => {
|
||||
const item = {
|
||||
id: 1,
|
||||
title: 'How to use product?',
|
||||
content:
|
||||
'This is a detailed explanation of how to use the product effectively.',
|
||||
products: ['Product A'],
|
||||
product_types: ['Type A'],
|
||||
};
|
||||
const result = converters.questions(item);
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
title: 'How to use product?',
|
||||
summary:
|
||||
'This is a detailed explanation of how to use the product effectively....',
|
||||
type: 'question',
|
||||
});
|
||||
});
|
||||
|
||||
test('convert product document item', () => {
|
||||
const item = {
|
||||
id: 1,
|
||||
title: 'User Manual',
|
||||
products: ['Product A'],
|
||||
product_types: ['Type A'],
|
||||
fileUUID: 'TEST-UUID',
|
||||
};
|
||||
const result = converters.product_documents(item);
|
||||
expect(result).toEqual({
|
||||
id: 'TEST-UUID',
|
||||
title: 'User Manual',
|
||||
summary: undefined,
|
||||
type: 'document',
|
||||
});
|
||||
});
|
||||
});
|
||||
36
server/utils/search-converters.ts
Normal file
36
server/utils/search-converters.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 各索引对应的转换函数表
|
||||
*/
|
||||
export const converters: {
|
||||
[K in keyof MeiliIndexMap]: (item: MeiliIndexMap[K]) => SearchItemView;
|
||||
} = {
|
||||
products: (item: MeiliIndexMap['products']): SearchItemView => ({
|
||||
id: item.id,
|
||||
type: 'product',
|
||||
title: item.name,
|
||||
summary: item.summary,
|
||||
}),
|
||||
|
||||
solutions: (item: MeiliIndexMap['solutions']): SearchItemView => ({
|
||||
id: item.id,
|
||||
type: 'solution',
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
}),
|
||||
|
||||
questions: (item: MeiliIndexMap['questions']): SearchItemView => ({
|
||||
id: item.id,
|
||||
type: 'question',
|
||||
title: item.title,
|
||||
summary: item.content.slice(0, 100) + '...',
|
||||
}),
|
||||
|
||||
product_documents: (
|
||||
item: MeiliIndexMap['product_documents']
|
||||
): SearchItemView => ({
|
||||
id: item.fileUUID || item.id,
|
||||
type: 'document',
|
||||
title: item.title,
|
||||
summary: '',
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user