Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,8 @@ export declare interface ThumbnailInfo {
value: string;
}
export declare type ThumbnailSize = 'large' | 'small' | 'middle' | 'origin';
export declare function updateAssetUserData(urlOrUuidOrPath: string, path: string, value: any): Promise<any>;
export declare function updateAssetUserData(urlOrUuidOrPath: string, userData: Record<string, any>): Promise<any>;
export declare function updateAssetUserDataByPath(urlOrUuidOrPath: string, path: string, value: any): Promise<any>;
export declare function updateDefaultUserData(handler: string, path: string, value: any): Promise<void>;
export declare interface Vec2 {
x: number;
Expand Down Expand Up @@ -7066,6 +7067,7 @@ export declare namespace Assets {
updateDefaultUserData,
queryAssetUserDataConfig,
updateAssetUserData,
updateAssetUserDataByPath,
queryAssetConfigMap,
queryPropertySchema,
queryThumbnailHandlers,
Expand Down Expand Up @@ -8454,7 +8456,8 @@ export declare interface ThumbnailInfo {
}
export declare type ThumbnailSize = 'large' | 'small' | 'middle' | 'origin';
export declare function unregister(): Promise<void>;
export declare function updateAssetUserData(urlOrUuidOrPath: string, path: string, value: any): Promise<any>;
export declare function updateAssetUserData(urlOrUuidOrPath: string, userData: Record<string, any>): Promise<any>;
export declare function updateAssetUserDataByPath(urlOrUuidOrPath: string, path: string, value: any): Promise<any>;
export declare function updateDefaultUserData(handler: string, path: string, value: any): Promise<void>;
export declare interface Vec2 {
x: number;
Expand Down
40 changes: 37 additions & 3 deletions src/api/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ import {
TAssetMoveOptions,
TAssetRenameOptions,
TUserDataHandler,
SchemaUpdateAssetUserData,
SchemaUpdateAssetUserDataPath,
SchemaUpdateAssetUserDataValue,
SchemaUpdateAssetUserDataResult,
TUpdateAssetUserData,
TUpdateAssetUserDataPath,
TUpdateAssetUserDataValue,
TUpdateAssetUserDataResult,
Expand Down Expand Up @@ -960,9 +962,41 @@ export class AssetsApi {
*/
@tool('assets-update-asset-user-data')
@title('Update Asset User Data') // 更新资源用户数据
@description('Update the userData of the specified asset via path and value. urlOrUuidOrPath accepts an asset URL, UUID, file path, or sub asset UUID in parentUuid@subMetaId format.') // 更新指定资源的用户数据配置。通过路径和值来精确更新资源的用户数据,支持嵌套路径访问
@description('Replace the complete userData object of the specified asset in one save. urlOrUuidOrPath accepts an asset URL, UUID, file path, or sub asset UUID in parentUuid@subMetaId format.') // 一次性整体替换指定资源的 userData,支持父资源与 parentUuid@subMetaId 子资源 UUID
@result(SchemaUpdateAssetUserDataResult)
async updateAssetUserData(
@param(SchemaUrlOrUUIDOrPath) urlOrUuidOrPath: TUrlOrUUIDOrPath,
@param(SchemaUpdateAssetUserData) userData: TUpdateAssetUserData
): Promise<CommonResultType<TUpdateAssetUserDataResult>> {
const code: HttpStatusCode = COMMON_STATUS.SUCCESS;
const ret: CommonResultType<TUpdateAssetUserDataResult> = {
code: code,
data: null,
};

try {
ret.data = await assetManager.updateUserData(urlOrUuidOrPath, userData);
if (!ret.data) {
ret.code = COMMON_STATUS.NOT_FOUND;
ret.reason = `❌Asset can not be found: ${urlOrUuidOrPath}. Please refresh asset db and try again.`;
}
} catch (e) {
ret.code = getCommonErrorStatus(e);
console.error('update asset user data fail:', e instanceof Error ? e.message : String(e));
ret.reason = e instanceof Error ? e.message : String(e);
}

return ret;
}

/**
* Update Asset User Data By Path // 按路径更新资源用户数据
*/
@tool('assets-update-asset-user-data-by-path')
@title('Update Asset User Data By Path') // 按路径更新资源用户数据
@description('Update a single path in the userData of the specified asset. urlOrUuidOrPath accepts an asset URL, UUID, file path, or sub asset UUID in parentUuid@subMetaId format.') // 通过路径和值精确更新指定资源 userData 的单个字段,支持父资源与 parentUuid@subMetaId 子资源 UUID。
@result(SchemaUpdateAssetUserDataResult)
async updateAssetUserDataByPath(
@param(SchemaUrlOrUUIDOrPath) urlOrUuidOrPath: TUrlOrUUIDOrPath,
@param(SchemaUpdateAssetUserDataPath) path: TUpdateAssetUserDataPath,
@param(SchemaUpdateAssetUserDataValue) value: TUpdateAssetUserDataValue
Expand All @@ -974,14 +1008,14 @@ export class AssetsApi {
};

try {
ret.data = await assetManager.updateUserData(urlOrUuidOrPath, path, value);
ret.data = await assetManager.updateUserDataByPath(urlOrUuidOrPath, path, value);
if (!ret.data) {
ret.code = COMMON_STATUS.NOT_FOUND;
ret.reason = `❌Asset can not be found: ${urlOrUuidOrPath}. Please refresh asset db and try again.`;
}
} catch (e) {
ret.code = getCommonErrorStatus(e);
console.error('update asset user data fail:', e instanceof Error ? e.message : String(e));
console.error('update asset user data by path fail:', e instanceof Error ? e.message : String(e));
ret.reason = e instanceof Error ? e.message : String(e);
}

Expand Down
3 changes: 3 additions & 0 deletions src/api/assets/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ export type TUpdateUserDataOptions = z.infer<typeof SchemaUpdateUserDataOptions>
export type TUserDataHandler = z.infer<typeof SchemaUserDataHandler>;

// Update Asset User Data related Schema // Update Asset User Data 相关 Schema
export const SchemaUpdateAssetUserData = z.record(z.string(), z.any()).describe('Complete asset userData object to replace the existing userData'); // 用于整体替换现有 userData 的完整资源 userData 对象
export type TUpdateAssetUserData = z.infer<typeof SchemaUpdateAssetUserData>;

export const SchemaUpdateAssetUserDataPath = z.string().min(1).describe('User data path, separated by dots, e.g. "texture.wrapMode"'); // 用户数据路径,使用点号分隔,如 "texture.wrapMode"
export type TUpdateAssetUserDataPath = z.infer<typeof SchemaUpdateAssetUserDataPath>;

Expand Down
2 changes: 2 additions & 0 deletions src/core/assets/manager/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class AssetManager extends EventEmitter {
outputExportData = assetOperation.outputExportData.bind(assetOperation);
createAssetByType = assetOperation.createAssetByType.bind(assetOperation);
updateUserData = assetOperation.updateUserData.bind(assetOperation);
updateUserDataByPath = assetOperation.updateUserDataByPath.bind(assetOperation);
querySerializedData = serializedData.querySerializedData;
saveSerializedData = serializedData.saveSerializedData;

Expand Down Expand Up @@ -362,6 +363,7 @@ export interface TypedAssetManager extends EventEmitter {
outputExportData: typeof assetOperation.outputExportData;
createAssetByType: typeof assetOperation.createAssetByType;
updateUserData: typeof assetOperation.updateUserData;
updateUserDataByPath: typeof assetOperation.updateUserDataByPath;
querySerializedData: typeof serializedData.querySerializedData;
saveSerializedData: typeof serializedData.saveSerializedData;

Expand Down
37 changes: 35 additions & 2 deletions src/core/assets/manager/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,48 @@ class AssetOperation extends EventEmitter {
await asset._assetDB.reimport(asset.uuid);
}

async updateUserData<T extends keyof AssetUserDataMap = 'unknown'>(uuidOrURLOrPath: string, path: string, value: any): Promise<AssetUserDataMap[T]> {
async updateUserData<T extends keyof AssetUserDataMap = 'unknown'>(uuidOrURLOrPath: string, userData: AssetUserDataMap[T]): Promise<AssetUserDataMap[T] | undefined> {
if (!isRecord(userData)) {
throw new Error('userData must be an object');
}

const asset = assetQuery.queryAsset(uuidOrURLOrPath);
if (!asset) {
console.error(`can not find asset ${uuidOrURLOrPath}`);
return;
}

if (!isRecord(asset.meta.userData)) {
asset.meta.userData = {} as AssetUserDataMap[T];
}
const currentUserData = asset.meta.userData as Record<string, unknown>;
for (const key of Object.keys(currentUserData)) {
delete currentUserData[key];
}
Object.assign(currentUserData, lodash.cloneDeep(userData));
asset.meta.userData = currentUserData as AssetUserDataMap[T];
await asset.save();
await asset._assetDB.reimport(asset.uuid);
return asset?.meta.userData as AssetUserDataMap[T];
}

async updateUserDataByPath<T extends keyof AssetUserDataMap = 'unknown'>(uuidOrURLOrPath: string, path: string, value: any): Promise<AssetUserDataMap[T] | undefined> {
if (!path) {
throw new Error('path must not be empty. Use updateUserData to replace the complete userData object');
}

const asset = assetQuery.queryAsset(uuidOrURLOrPath);
if (!asset) {
console.error(`can not find asset ${uuidOrURLOrPath}`);
return;
}
if (!isRecord(asset.meta.userData)) {
asset.meta.userData = {} as AssetUserDataMap[T];
}
lodash.set(asset?.meta.userData, path, value);
await asset.save();
return asset?.meta.userData;
await asset._assetDB.reimport(asset.uuid);
return asset?.meta.userData as AssetUserDataMap[T];
}

async saveAsset(uuidOrURLOrPath: string, content: string | Buffer) {
Expand Down
51 changes: 49 additions & 2 deletions src/core/assets/test/operation-filesystem-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,15 @@ describe('asset operation filesystem bridge', () => {
jest.restoreAllMocks();
});

it('updateUserData updates sub asset userData through composite uuid without reimport', async () => {
it('updateUserData replaces sub asset userData through composite uuid with one reimport', async () => {
const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation');
const reimport = jest.fn();
const subAsset = {
uuid: '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
meta: {
userData: {
minfilter: 'linear',
obsolete: true,
},
},
save: jest.fn().mockResolvedValue(true),
Expand All @@ -183,7 +184,41 @@ describe('asset operation filesystem bridge', () => {
};
mockQueryAsset.mockReturnValue(subAsset);

const userData = {
minfilter: 'nearest',
wrapMode: 'clamp',
};
const result = await assetOperation.updateUserData(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
userData,
);

expect(mockQueryAsset).toHaveBeenCalledWith('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a');
expect(subAsset.meta.userData).toEqual(userData);
expect(subAsset.save).toHaveBeenCalledTimes(1);
expect(reimport).toHaveBeenCalledTimes(1);
expect(reimport).toHaveBeenCalledWith('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a');
expect(result).toBe(subAsset.meta.userData);
});

it('updateUserDataByPath updates sub asset userData through composite uuid with one reimport', async () => {
const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation');
const reimport = jest.fn();
const subAsset = {
uuid: '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
meta: {
userData: {
minfilter: 'linear',
},
},
save: jest.fn().mockResolvedValue(true),
_assetDB: {
reimport,
},
};
mockQueryAsset.mockReturnValue(subAsset);

const result = await assetOperation.updateUserDataByPath(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
'minfilter',
'nearest',
Expand All @@ -194,10 +229,22 @@ describe('asset operation filesystem bridge', () => {
minfilter: 'nearest',
});
expect(subAsset.save).toHaveBeenCalledTimes(1);
expect(reimport).not.toHaveBeenCalled();
expect(reimport).toHaveBeenCalledTimes(1);
expect(reimport).toHaveBeenCalledWith('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a');
expect(result).toBe(subAsset.meta.userData);
});

it('updateUserDataByPath rejects empty path instead of replacing complete userData', async () => {
const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation');

await expect(assetOperation.updateUserDataByPath(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
'',
{ minfilter: 'nearest' },
)).rejects.toThrow('path must not be empty');
expect(mockQueryAsset).not.toHaveBeenCalled();
});

it('renameAsset should delegate rename steps to filesystem bridge', async () => {
const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation');
const source = 'D:/project/assets/source.txt';
Expand Down
12 changes: 11 additions & 1 deletion src/lib/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,21 @@ export async function queryAssetUserDataConfig(
* Update Asset User Data // 更新资源用户数据
*/
export async function updateAssetUserData(
urlOrUuidOrPath: string,
userData: Record<string, any>
): Promise<any> {
return await assetManager.updateUserData(urlOrUuidOrPath, userData);
}

/**
* Update Asset User Data By Path // 按路径更新资源用户数据
*/
export async function updateAssetUserDataByPath(
urlOrUuidOrPath: string,
path: string,
value: any
): Promise<any> {
return await assetManager.updateUserData(urlOrUuidOrPath, path, value);
return await assetManager.updateUserDataByPath(urlOrUuidOrPath, path, value);
}

/**
Expand Down
29 changes: 26 additions & 3 deletions tests/assets-update-asset-user-data-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const mockUpdateUserData = jest.fn();
const mockUpdateUserDataByPath = jest.fn();

jest.mock('../src/core/assets', () => ({
assetDBManager: {},
assetManager: {
updateUserData: (...args: unknown[]) => mockUpdateUserData(...args),
updateUserDataByPath: (...args: unknown[]) => mockUpdateUserDataByPath(...args),
},
}));

Expand All @@ -16,6 +18,7 @@ import { AssetsApi } from '../src/api/assets/assets';
describe('assets-update-asset-user-data api', () => {
beforeEach(() => {
mockUpdateUserData.mockReset();
mockUpdateUserDataByPath.mockReset();
});

it('does not register a separate meta userData update tool', () => {
Expand All @@ -30,11 +33,31 @@ describe('assets-update-asset-user-data api', () => {
expect(schema!.parse('6FA5FBAD0D324B6395D824507665775C@6C48A')).toBe('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a');
});

it('delegates sub asset UUID updates to assetManager.updateUserData', async () => {
const updatedUserData = { minfilter: 'nearest' };
it('delegates complete userData replacement to assetManager.updateUserData', async () => {
const userData = { minfilter: 'nearest', wrapMode: 'clamp' };
const updatedUserData = { ...userData };
mockUpdateUserData.mockResolvedValue(updatedUserData);

const result = await new AssetsApi().updateAssetUserData(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
userData,
);

expect(result).toEqual({
code: COMMON_STATUS.SUCCESS,
data: updatedUserData,
});
expect(mockUpdateUserData).toHaveBeenCalledWith(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
userData,
);
});

it('delegates path updates to assetManager.updateUserDataByPath', async () => {
const updatedUserData = { minfilter: 'nearest' };
mockUpdateUserDataByPath.mockResolvedValue(updatedUserData);

const result = await new AssetsApi().updateAssetUserDataByPath(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
'minfilter',
'nearest',
Expand All @@ -44,7 +67,7 @@ describe('assets-update-asset-user-data api', () => {
code: COMMON_STATUS.SUCCESS,
data: updatedUserData,
});
expect(mockUpdateUserData).toHaveBeenCalledWith(
expect(mockUpdateUserDataByPath).toHaveBeenCalledWith(
'6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a',
'minfilter',
'nearest',
Expand Down
31 changes: 26 additions & 5 deletions tests/lib/assets-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ describe('lib assets api', () => {
expect((Assets as { updateAssetMetaUserData?: unknown }).updateAssetMetaUserData).toBeUndefined();
});

it('updateAssetUserData delegates sub asset uuid to assetManager', async () => {
const result = { minfilter: 'nearest' };
it('updateAssetUserData delegates complete userData replacement to assetManager', async () => {
const userData = { minfilter: 'nearest', wrapMode: 'clamp' };
const result = { ...userData };
const spy = jest.spyOn(assetManager, 'updateUserData').mockResolvedValue(result);
const updateAssetUserData = (Assets as {
updateAssetUserData?: (
urlOrUuidOrPath: string,
path: string,
value: unknown
userData: Record<string, unknown>
) => Promise<unknown>;
}).updateAssetUserData;

Expand All @@ -31,7 +31,28 @@ describe('lib assets api', () => {
throw new Error('updateAssetUserData is not exposed from lib/assets/assets');
}

await expect(updateAssetUserData('parent-uuid@6c48a', 'minfilter', 'nearest')).resolves.toBe(result);
await expect(updateAssetUserData('parent-uuid@6c48a', userData)).resolves.toBe(result);
expect(spy).toHaveBeenCalledWith('parent-uuid@6c48a', userData);
});

it('updateAssetUserDataByPath delegates path updates to assetManager', async () => {
const result = { minfilter: 'nearest' };
const spy = jest.spyOn(assetManager, 'updateUserDataByPath').mockResolvedValue(result);
const updateAssetUserDataByPath = (Assets as {
updateAssetUserDataByPath?: (
urlOrUuidOrPath: string,
path: string,
value: unknown
) => Promise<unknown>;
}).updateAssetUserDataByPath;

expect(updateAssetUserDataByPath).toEqual(expect.any(Function));

if (!updateAssetUserDataByPath) {
throw new Error('updateAssetUserDataByPath is not exposed from lib/assets/assets');
}

await expect(updateAssetUserDataByPath('parent-uuid@6c48a', 'minfilter', 'nearest')).resolves.toBe(result);
expect(spy).toHaveBeenCalledWith('parent-uuid@6c48a', 'minfilter', 'nearest');
});

Expand Down
Loading