[MM-62565] DB only custom profile attributes and fields (#8739)
Database only
This commit is contained in:
parent
74ee271012
commit
6a060cd505
33 changed files with 1952 additions and 18 deletions
|
|
@ -25,7 +25,7 @@ import {fetchGroupsByNames} from './groups';
|
|||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type {CustomAttribute, CustomProfileAttributeSimple, CustomProfileField, CustomAttributeSet} from '@typings/api/custom_profile_attributes';
|
||||
import type {CustomAttribute, CustomProfileField, CustomAttributeSet, UserCustomProfileAttributeSimple} from '@typings/api/custom_profile_attributes';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type MyUserRequest = {
|
||||
|
|
@ -914,7 +914,7 @@ export const fetchCustomAttributes = async (serverUrl: string, userId: string, f
|
|||
export const updateCustomAttributes = async (serverUrl: string, attributes: CustomAttributeSet): Promise<{success: boolean; error: unknown}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const values: CustomProfileAttributeSimple = {};
|
||||
const values: UserCustomProfileAttributeSimple = {};
|
||||
Object.keys(attributes).forEach((field) => {
|
||||
values[field] = attributes[field].value;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import type ClientBase from './base';
|
||||
import type {CustomProfileAttributeSimple, CustomProfileField} from '@typings/api/custom_profile_attributes';
|
||||
import type {CustomProfileField, UserCustomProfileAttributeSimple} from '@typings/api/custom_profile_attributes';
|
||||
|
||||
export interface ClientCustomAttributesMix {
|
||||
getCustomProfileAttributeFields: () => Promise<CustomProfileField[]>;
|
||||
getCustomProfileAttributeValues: (userID: string) => Promise<CustomProfileAttributeSimple>;
|
||||
updateCustomProfileAttributeValues: (values: CustomProfileAttributeSimple) => Promise<string>;
|
||||
getCustomProfileAttributeValues: (userID: string) => Promise<UserCustomProfileAttributeSimple>;
|
||||
updateCustomProfileAttributeValues: (values: UserCustomProfileAttributeSimple) => Promise<string>;
|
||||
}
|
||||
|
||||
const ClientCustomAttributes = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -24,7 +24,7 @@ const ClientCustomAttributes = <TBase extends Constructor<ClientBase>>(superclas
|
|||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
updateCustomProfileAttributeValues = async (values: CustomProfileAttributeSimple) => {
|
||||
updateCustomProfileAttributeValues = async (values: UserCustomProfileAttributeSimple) => {
|
||||
return this.doFetch(
|
||||
`${this.getCustomProfileAttributesRoute()}/values`,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export const MM_TABLES = {
|
|||
CHANNEL_MEMBERSHIP: 'ChannelMembership',
|
||||
CONFIG: 'Config',
|
||||
CUSTOM_EMOJI: 'CustomEmoji',
|
||||
CUSTOM_PROFILE_FIELD: 'CustomProfileField',
|
||||
CUSTOM_PROFILE_ATTRIBUTE: 'CustomProfileAttribute',
|
||||
DRAFT: 'Draft',
|
||||
FILE: 'File',
|
||||
GROUP: 'Group',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
|||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, CustomProfileFieldModel, CustomProfileAttributeModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -47,7 +47,7 @@ class DatabaseManagerSingleton {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, CustomProfileFieldModel, CustomProfileAttributeModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
|||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, CustomProfileFieldModel, CustomProfileAttributeModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -44,7 +44,7 @@ class DatabaseManagerSingleton {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, CustomProfileFieldModel, CustomProfileAttributeModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,36 @@ import {addColumns, createTable, schemaMigrations, unsafeExecuteSql} from '@nozb
|
|||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
const {CHANNEL_BOOKMARK, CHANNEL_INFO, DRAFT, POST, CHANNEL} = MM_TABLES.SERVER;
|
||||
const {CHANNEL_BOOKMARK, CHANNEL_INFO, DRAFT, POST, CHANNEL, CUSTOM_PROFILE_ATTRIBUTE, CUSTOM_PROFILE_FIELD} = MM_TABLES.SERVER;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 8,
|
||||
steps: [
|
||||
createTable({
|
||||
name: CUSTOM_PROFILE_ATTRIBUTE,
|
||||
columns: [
|
||||
{name: 'field_id', type: 'string', isIndexed: true},
|
||||
{name: 'user_id', type: 'string', isIndexed: true},
|
||||
{name: 'value', type: 'string'},
|
||||
],
|
||||
}),
|
||||
createTable({
|
||||
name: CUSTOM_PROFILE_FIELD,
|
||||
columns: [
|
||||
{name: 'group_id', type: 'string', isIndexed: true},
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'target_id', type: 'string'},
|
||||
{name: 'target_type', type: 'string'},
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number', isOptional: true},
|
||||
{name: 'attrs', type: 'string', isOptional: true},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 7,
|
||||
steps: [
|
||||
|
|
|
|||
44
app/database/models/server/custom_profile_attribute.ts
Normal file
44
app/database/models/server/custom_profile_attribute.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type CustomProfileFieldModel from './custom_profile_field';
|
||||
import type UserModel from './user';
|
||||
import type {Relation} from '@nozbe/watermelondb';
|
||||
import type CustomProfileAttributeModelInterface from '@typings/database/models/servers/custom_profile_attribute';
|
||||
|
||||
const {CUSTOM_PROFILE_ATTRIBUTE, CUSTOM_PROFILE_FIELD, USER} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* The CustomProfileAttribute model represents the 'CUSTOM_PROFILE_ATTRIBUTE' table and stores
|
||||
* the custom profile attribute values for users.
|
||||
*/
|
||||
export default class CustomProfileAttributeModel extends Model implements CustomProfileAttributeModelInterface {
|
||||
/** table (name) : CustomProfileAttribute */
|
||||
static table = CUSTOM_PROFILE_ATTRIBUTE;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
[CUSTOM_PROFILE_FIELD]: {type: 'belongs_to', key: 'field_id'},
|
||||
[USER]: {type: 'belongs_to', key: 'user_id'},
|
||||
};
|
||||
|
||||
/** field_id : The identifier of the custom profile field this attribute is for */
|
||||
@field('field_id') fieldId!: string;
|
||||
|
||||
/** user_id : The identifier of the user this attribute belongs to */
|
||||
@field('user_id') userId!: string;
|
||||
|
||||
/** value : The value of the custom profile attribute */
|
||||
@field('value') value!: string;
|
||||
|
||||
/** Relation to the custom profile field */
|
||||
@immutableRelation(CUSTOM_PROFILE_FIELD, 'field_id') field!: Relation<CustomProfileFieldModel>;
|
||||
|
||||
/** Relation to the user */
|
||||
@immutableRelation(USER, 'user_id') user!: Relation<UserModel>;
|
||||
}
|
||||
61
app/database/models/server/custom_profile_field.ts
Normal file
61
app/database/models/server/custom_profile_field.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {field, json, children} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
|
||||
import type CustomProfileAttributeModel from './custom_profile_attribute';
|
||||
import type {Query} from '@nozbe/watermelondb';
|
||||
import type CustomProfileFieldModelInterface from '@typings/database/models/servers/custom_profile_field';
|
||||
|
||||
const {CUSTOM_PROFILE_ATTRIBUTE, CUSTOM_PROFILE_FIELD} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* The CustomProfileField model represents the 'CUSTOM_PROFILE_FIELD' table and defines
|
||||
* the custom profile fields available in the system.
|
||||
*/
|
||||
export default class CustomProfileFieldModel extends Model implements CustomProfileFieldModelInterface {
|
||||
/** table (name) : CustomProfileField */
|
||||
static table = CUSTOM_PROFILE_FIELD;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
[CUSTOM_PROFILE_ATTRIBUTE]: {type: 'has_many', foreignKey: 'field_id'},
|
||||
};
|
||||
|
||||
/** group_id : The identifier of the group this field belongs to */
|
||||
@field('group_id') groupId!: string;
|
||||
|
||||
/** name : The name of the custom profile field */
|
||||
@field('name') name!: string;
|
||||
|
||||
/** type : The type of values accepted (e.g., 'text') */
|
||||
@field('type') type!: string;
|
||||
|
||||
/** target_id : The id of the target element (empty if system property) */
|
||||
@field('target_id') targetId!: string;
|
||||
|
||||
/** target_type : The type of element this is assigned to (e.g., 'user', 'post', 'card') */
|
||||
@field('target_type') targetType!: string;
|
||||
|
||||
/** create_at : The timestamp when this field was created */
|
||||
@field('create_at') createAt!: number;
|
||||
|
||||
/** update_at : The timestamp when this field was last updated */
|
||||
@field('update_at') updateAt!: number;
|
||||
|
||||
/** delete_at : The timestamp when this field was deleted (0 if not deleted) */
|
||||
@field('delete_at') deleteAt!: number;
|
||||
|
||||
/** attrs : Any extra properties of the field */
|
||||
@json('attrs', safeParseJSON) attrs!: {
|
||||
sort_order?: number;
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
|
||||
/** customProfileAttributes : All the custom profile attributes for this field */
|
||||
@children(CUSTOM_PROFILE_ATTRIBUTE) customProfileAttributes!: Query<CustomProfileAttributeModel>;
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ export {default as ChannelInfoModel} from './channel_info';
|
|||
export {default as ChannelMembershipModel} from './channel_membership';
|
||||
export {default as ConfigModel} from './config';
|
||||
export {default as CustomEmojiModel} from './custom_emoji';
|
||||
export {default as CustomProfileFieldModel} from './custom_profile_field';
|
||||
export {default as CustomProfileAttributeModel} from './custom_profile_attribute';
|
||||
export {default as DraftModel} from './draft';
|
||||
export {default as FileModel} from './file';
|
||||
export {default as GroupModel} from './group';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
|||
import {MM_TABLES} from '@constants/database';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
|
||||
import type CustomProfileAttributeModel from './custom_profile_attribute';
|
||||
import type {Query} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
|
@ -22,6 +23,7 @@ const {
|
|||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
CUSTOM_PROFILE_ATTRIBUTE,
|
||||
POST,
|
||||
PREFERENCE,
|
||||
REACTION,
|
||||
|
|
@ -64,6 +66,9 @@ export default class UserModel extends Model implements UserModelInterface {
|
|||
|
||||
/** USER has a 1:N relationship with THREAD_PARTICIPANT. A user can participate in multiple threads */
|
||||
[THREAD_PARTICIPANT]: {type: 'has_many', foreignKey: 'user_id'},
|
||||
|
||||
/** USER has a 1:N relationship with CUSTOM_PROFILE_ATTRIBUTE. A user can have multiple custom profile attributes */
|
||||
[CUSTOM_PROFILE_ATTRIBUTE]: {type: 'has_many', foreignKey: 'user_id'},
|
||||
};
|
||||
|
||||
/** auth_service : The type of authentication service registered to that user */
|
||||
|
|
@ -152,6 +157,9 @@ export default class UserModel extends Model implements UserModelInterface {
|
|||
/** threadParticipations : All the thread participations this user is part of */
|
||||
@children(THREAD_PARTICIPANT) threadParticipations!: Query<ThreadParticipantsModel>;
|
||||
|
||||
/** USER has a 1:N relationship with CUSTOM_PROFILE_ATTRIBUTE. A user can have multiple custom profile attributes */
|
||||
@children(CUSTOM_PROFILE_ATTRIBUTE) customProfileAttributes!: Query<CustomProfileAttributeModel> | undefined;
|
||||
|
||||
prepareStatus = (status: string) => {
|
||||
this.prepareUpdate((u) => {
|
||||
u.status = status;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ export default class BaseDataOperator {
|
|||
|
||||
if (createOrUpdateRawValues.length > 0) {
|
||||
for (const newElement of createOrUpdateRawValues) {
|
||||
// @ts-expect-error object with string key
|
||||
const key = buildKeyRecordBy?.(newElement) || newElement[fieldName];
|
||||
const existingRecord = recordsByKeys[key];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {
|
||||
transformCustomProfileFieldRecord,
|
||||
transformCustomProfileAttributeRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/custom_profile';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {CustomProfileField, CustomProfileAttribute} from '@typings/api/custom_profile_attributes';
|
||||
|
||||
describe('*** Operator: Custom Profile Handlers tests ***', () => {
|
||||
let operator: ServerDataOperator;
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
||||
});
|
||||
describe('=> handleCustomProfileFields', () => {
|
||||
it('should write to CustomProfileField table', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
|
||||
const fields: CustomProfileField[] = [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
create_at: 1607683720173,
|
||||
update_at: 1607683720173,
|
||||
delete_at: 0,
|
||||
group_id: 'group1',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
attrs: {},
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
createOrUpdateRawValues: fields,
|
||||
tableName: MM_TABLES.SERVER.CUSTOM_PROFILE_FIELD,
|
||||
prepareRecordsOnly: false,
|
||||
transformer: transformCustomProfileFieldRecord,
|
||||
}, 'handleCustomProfileFields');
|
||||
});
|
||||
|
||||
it('should properly pass prepareRecordsOnly when set to true', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const originalHandleRecords = operator.handleRecords;
|
||||
operator.handleRecords = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const fields: CustomProfileField[] = [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
create_at: 1607683720173,
|
||||
update_at: 1607683720173,
|
||||
delete_at: 0,
|
||||
group_id: 'group1',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
attrs: {},
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(operator.handleRecords).toHaveBeenCalledTimes(1);
|
||||
expect(operator.handleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
createOrUpdateRawValues: fields,
|
||||
tableName: MM_TABLES.SERVER.CUSTOM_PROFILE_FIELD,
|
||||
prepareRecordsOnly: true,
|
||||
transformer: transformCustomProfileFieldRecord,
|
||||
}, 'handleCustomProfileFields');
|
||||
|
||||
operator.handleRecords = originalHandleRecords;
|
||||
});
|
||||
|
||||
it('should handle empty fields array', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = await operator.handleCustomProfileFields({
|
||||
fields: [],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle duplicate fields by using unique id', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
|
||||
const fields: CustomProfileField[] = [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
create_at: 1607683720173,
|
||||
update_at: 1607683720173,
|
||||
delete_at: 0,
|
||||
group_id: 'group1',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
attrs: {},
|
||||
},
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 2',
|
||||
type: 'text',
|
||||
create_at: 1607683720173,
|
||||
update_at: 1607683720173,
|
||||
delete_at: 0,
|
||||
group_id: 'group1',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
attrs: {},
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
createOrUpdateRawValues: [fields[1]],
|
||||
}),
|
||||
'handleCustomProfileFields',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('=> handleCustomProfileAttributes', () => {
|
||||
it('should write to CustomProfileAttribute table', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
|
||||
const attributes: CustomProfileAttribute[] = [
|
||||
{
|
||||
id: 'field1-user1',
|
||||
field_id: 'field1',
|
||||
user_id: 'user1',
|
||||
value: 'Test Value',
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
createOrUpdateRawValues: attributes,
|
||||
tableName: MM_TABLES.SERVER.CUSTOM_PROFILE_ATTRIBUTE,
|
||||
prepareRecordsOnly: false,
|
||||
transformer: transformCustomProfileAttributeRecord,
|
||||
}, 'handleCustomProfileAttributes');
|
||||
});
|
||||
|
||||
it('should properly pass prepareRecordsOnly when set to true', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const originalHandleRecords = operator.handleRecords;
|
||||
operator.handleRecords = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const attributes: CustomProfileAttribute[] = [
|
||||
{
|
||||
id: 'field1-user1',
|
||||
field_id: 'field1',
|
||||
user_id: 'user1',
|
||||
value: 'Test Value',
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
expect(operator.handleRecords).toHaveBeenCalledTimes(1);
|
||||
expect(operator.handleRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
createOrUpdateRawValues: attributes,
|
||||
tableName: MM_TABLES.SERVER.CUSTOM_PROFILE_ATTRIBUTE,
|
||||
prepareRecordsOnly: true,
|
||||
transformer: transformCustomProfileAttributeRecord,
|
||||
}, 'handleCustomProfileAttributes');
|
||||
|
||||
// Restore original method
|
||||
operator.handleRecords = originalHandleRecords;
|
||||
});
|
||||
|
||||
it('should handle empty attributes array', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = await operator.handleCustomProfileAttributes({
|
||||
attributes: [],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle duplicate attributes by using unique id', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleRecords = jest.spyOn(operator, 'handleRecords');
|
||||
const attributes: CustomProfileAttribute[] = [
|
||||
{
|
||||
id: 'field1-user1',
|
||||
field_id: 'field1',
|
||||
user_id: 'user1',
|
||||
value: 'Test Value 1',
|
||||
},
|
||||
{
|
||||
id: 'field1-user1',
|
||||
field_id: 'field1',
|
||||
user_id: 'user1',
|
||||
value: 'Test Value 2',
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
createOrUpdateRawValues: [attributes[1]],
|
||||
}),
|
||||
'handleCustomProfileAttributes',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {
|
||||
transformCustomProfileFieldRecord,
|
||||
transformCustomProfileAttributeRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/custom_profile';
|
||||
import {getUniqueRawsBy} from '@database/operator/utils/general';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type ServerDataOperatorBase from '.';
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type {
|
||||
HandleCustomProfileFieldsArgs,
|
||||
HandleCustomProfileAttributesArgs,
|
||||
} from '@typings/database/database';
|
||||
|
||||
const {CUSTOM_PROFILE_FIELD, CUSTOM_PROFILE_ATTRIBUTE} = MM_TABLES.SERVER;
|
||||
|
||||
export interface CustomProfileHandlerMix {
|
||||
handleCustomProfileFields: ({fields, prepareRecordsOnly}: HandleCustomProfileFieldsArgs) => Promise<Model[]>;
|
||||
handleCustomProfileAttributes: ({attributes, prepareRecordsOnly}: HandleCustomProfileAttributesArgs) => Promise<Model[]>;
|
||||
}
|
||||
|
||||
const CustomProfileHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superclass: TBase) => class extends superclass {
|
||||
/**
|
||||
* handleCustomProfileFields: Handler responsible for the Create/Update operations occurring on the CUSTOM_PROFILE_FIELD table from the 'Server' schema
|
||||
* @param {HandleCustomProfileFieldsArgs} fieldsArgs
|
||||
* @param {CustomProfileField[]} fieldsArgs.fields
|
||||
* @param {boolean} fieldsArgs.prepareRecordsOnly
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
handleCustomProfileFields = async ({fields, prepareRecordsOnly = true}: HandleCustomProfileFieldsArgs): Promise<Model[]> => {
|
||||
if (!fields?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "fields" array has been passed to the handleCustomProfileFields method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: fields, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformCustomProfileFieldRecord,
|
||||
createOrUpdateRawValues,
|
||||
tableName: CUSTOM_PROFILE_FIELD,
|
||||
prepareRecordsOnly,
|
||||
}, 'handleCustomProfileFields');
|
||||
};
|
||||
|
||||
/**
|
||||
* handleCustomProfileAttributes: Handler responsible for the Create/Update operations occurring on the CUSTOM_PROFILE_ATTRIBUTE table from the 'Server' schema
|
||||
* @param {HandleCustomProfileAttributesArgs} attributesArgs
|
||||
* @param {CustomProfileAttributeSimple[]} attributesArgs.attributes
|
||||
* @param {boolean} attributesArgs.prepareRecordsOnly
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
handleCustomProfileAttributes = async ({attributes, prepareRecordsOnly = true}: HandleCustomProfileAttributesArgs): Promise<Model[]> => {
|
||||
if (!attributes?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "attributes" array has been passed to the handleCustomProfileAttributes method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: attributes, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformCustomProfileAttributeRecord,
|
||||
createOrUpdateRawValues,
|
||||
tableName: CUSTOM_PROFILE_ATTRIBUTE,
|
||||
prepareRecordsOnly,
|
||||
}, 'handleCustomProfileAttributes');
|
||||
};
|
||||
};
|
||||
|
||||
export default CustomProfileHandler;
|
||||
|
|
@ -123,7 +123,7 @@ const UserHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercla
|
|||
|
||||
const createOrUpdateRawValues = getUniqueRawsBy({raws: users, key: 'id'});
|
||||
|
||||
return this.handleRecords({
|
||||
const userModels = await this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformUserRecord,
|
||||
createOrUpdateRawValues,
|
||||
|
|
@ -131,6 +131,8 @@ const UserHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercla
|
|||
prepareRecordsOnly,
|
||||
shouldUpdate: shouldUpdateUserRecord,
|
||||
}, 'handleUsers');
|
||||
|
||||
return userModels;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import ServerDataOperatorBase from '@database/operator/server_data_operator/handlers';
|
||||
import CategoryHandler, {type CategoryHandlerMix} from '@database/operator/server_data_operator/handlers/category';
|
||||
import ChannelHandler, {type ChannelHandlerMix} from '@database/operator/server_data_operator/handlers/channel';
|
||||
import CustomProfileHandler, {type CustomProfileHandlerMix} from '@database/operator/server_data_operator/handlers/custom_profile';
|
||||
import GroupHandler, {type GroupHandlerMix} from '@database/operator/server_data_operator/handlers/group';
|
||||
import PostHandler, {type PostHandlerMix} from '@database/operator/server_data_operator/handlers/post';
|
||||
import TeamHandler, {type TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
|
||||
|
|
@ -17,6 +18,7 @@ import type {Database} from '@nozbe/watermelondb';
|
|||
interface ServerDataOperator extends
|
||||
CategoryHandlerMix,
|
||||
ChannelHandlerMix,
|
||||
CustomProfileHandlerMix,
|
||||
GroupHandlerMix,
|
||||
PostHandlerMix,
|
||||
ServerDataOperatorBase,
|
||||
|
|
@ -29,6 +31,7 @@ interface ServerDataOperator extends
|
|||
class ServerDataOperator extends mix(ServerDataOperatorBase).with(
|
||||
CategoryHandler,
|
||||
ChannelHandler,
|
||||
CustomProfileHandler,
|
||||
GroupHandler,
|
||||
PostHandler,
|
||||
TeamHandler,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {OperationType, MM_TABLES} from '@constants/database';
|
||||
import {transformCustomProfileFieldRecord, transformCustomProfileAttributeRecord} from '@database/operator/server_data_operator/transformers/custom_profile';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
|
||||
import type {CustomProfileFieldModel} from '@database/models/server';
|
||||
import type {CustomProfileAttribute} from '@typings/api/custom_profile_attributes';
|
||||
|
||||
describe('*** CUSTOM PROFILE Prepare Records Test ***', () => {
|
||||
describe('=> transformCustomProfileFieldRecord', () => {
|
||||
it('should return a record of type CustomProfileField', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'custom_profile_field_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecord = await transformCustomProfileFieldRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'field1',
|
||||
group_id: 'group1',
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1596032651748,
|
||||
delete_at: 0,
|
||||
attrs: {required: true},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord!.collection.table).toBe(MM_TABLES.SERVER.CUSTOM_PROFILE_FIELD);
|
||||
});
|
||||
|
||||
it('should handle update action', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'custom_profile_field_update_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const mockRecord = {
|
||||
id: 'field1',
|
||||
groupId: 'group1',
|
||||
name: 'Original Field',
|
||||
type: 'text',
|
||||
targetId: 'target1',
|
||||
targetType: 'user',
|
||||
createAt: 1596032651748,
|
||||
updateAt: 1596032651748,
|
||||
deleteAt: 0,
|
||||
attrs: {required: true},
|
||||
_raw: {},
|
||||
_isEditing: false,
|
||||
_preparedState: null,
|
||||
customProfileAttributes: [],
|
||||
prepare: jest.fn(),
|
||||
observe: jest.fn(),
|
||||
update: jest.fn(),
|
||||
destroyPermanently: jest.fn(),
|
||||
markAsDeleted: jest.fn(),
|
||||
prepareUpdate: jest.fn().mockImplementation((callback) => {
|
||||
callback();
|
||||
return mockRecord;
|
||||
}),
|
||||
collection: {table: 'CustomProfileField'},
|
||||
} as unknown as CustomProfileFieldModel;
|
||||
|
||||
const preparedRecord = await transformCustomProfileFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: mockRecord,
|
||||
raw: {
|
||||
id: 'field1',
|
||||
group_id: 'group1',
|
||||
name: 'Updated Field',
|
||||
type: 'text',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1596032651749,
|
||||
delete_at: 0,
|
||||
attrs: {required: false},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord!.collection.table).toBe('CustomProfileField');
|
||||
});
|
||||
|
||||
it('should return a record of type CustomProfileAttribute', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'custom_profile_attribute_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecord = await transformCustomProfileAttributeRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'field1-user1',
|
||||
field_id: 'field1',
|
||||
user_id: 'user1',
|
||||
value: 'Test Value',
|
||||
} as CustomProfileAttribute,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord!.collection.table).toBe('CustomProfileAttribute');
|
||||
expect(preparedRecord!.id).toBe('field1-user1');
|
||||
});
|
||||
|
||||
it('should throw error for non-create action without record', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'custom_profile_field_error_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
try {
|
||||
await transformCustomProfileFieldRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'field1',
|
||||
group_id: 'group1',
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
target_id: 'target1',
|
||||
target_type: 'user',
|
||||
create_at: 1596032651748,
|
||||
update_at: 1596032651748,
|
||||
delete_at: 0,
|
||||
attrs: {required: true},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Should not reach here
|
||||
expect(true).toBe(false);
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBe('Record not found for non create action');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES, OperationType} from '@constants/database';
|
||||
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
||||
import {customProfileAttributeId} from '@utils/custom_profile_attribute';
|
||||
|
||||
import type {CustomProfileAttributeModel, CustomProfileFieldModel} from '@database/models/server';
|
||||
import type {CustomProfileField, CustomProfileAttribute} from '@typings/api/custom_profile_attributes';
|
||||
import type {TransformerArgs} from '@typings/database/database';
|
||||
|
||||
const {CUSTOM_PROFILE_FIELD, CUSTOM_PROFILE_ATTRIBUTE} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* transformCustomProfileFieldRecord: Prepares a record of the SERVER database 'CustomProfileField' table for update or create actions.
|
||||
* @param {TransformerArgs<CustomProfileFieldModel, CustomProfileField>} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<CustomProfileFieldModel>}
|
||||
*/
|
||||
export const transformCustomProfileFieldRecord = ({action, database, value}: TransformerArgs<CustomProfileFieldModel, CustomProfileField>): Promise<CustomProfileFieldModel> => {
|
||||
const raw = value.raw;
|
||||
const record = value.record;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
if (!isCreateAction && !record) {
|
||||
throw new Error('Record not found for non create action');
|
||||
}
|
||||
|
||||
const fieldsMapper = (field: CustomProfileFieldModel) => {
|
||||
field._raw.id = isCreateAction ? raw.id : record!.id;
|
||||
field.groupId = raw.group_id;
|
||||
field.name = raw.name;
|
||||
field.type = raw.type;
|
||||
field.targetId = raw.target_id;
|
||||
field.targetType = raw.target_type;
|
||||
field.createAt = raw.create_at;
|
||||
field.updateAt = raw.update_at;
|
||||
field.deleteAt = raw.delete_at;
|
||||
field.attrs = raw.attrs || null;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: CUSTOM_PROFILE_FIELD,
|
||||
value,
|
||||
fieldsMapper,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* transformCustomProfileAttributeRecord: Prepares a record of the SERVER database 'CustomProfileAttribute' table for update or create actions.
|
||||
* @param {TransformerArgs<CustomProfileAttributeModel, CustomProfileAttribute>} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<CustomProfileAttributeModel>}
|
||||
*/
|
||||
export const transformCustomProfileAttributeRecord = ({action, database, value}: TransformerArgs<CustomProfileAttributeModel, CustomProfileAttribute>): Promise<CustomProfileAttributeModel> => {
|
||||
const raw = value.raw as CustomProfileAttribute;
|
||||
|
||||
const fieldsMapper = (attribute: CustomProfileAttributeModel) => {
|
||||
attribute._raw.id = customProfileAttributeId(raw.field_id, raw.user_id);
|
||||
attribute.fieldId = raw.field_id;
|
||||
attribute.userId = raw.user_id;
|
||||
attribute.value = raw.value;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: CUSTOM_PROFILE_ATTRIBUTE,
|
||||
value,
|
||||
fieldsMapper,
|
||||
});
|
||||
};
|
||||
|
|
@ -12,6 +12,8 @@ import {
|
|||
ChannelMembershipSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
CustomProfileFieldSchema,
|
||||
CustomProfileAttributeSchema,
|
||||
DraftSchema,
|
||||
FileSchema,
|
||||
GroupSchema,
|
||||
|
|
@ -40,7 +42,7 @@ import {
|
|||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 7,
|
||||
version: 8,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
|
|
@ -50,6 +52,8 @@ export const serverSchema: AppSchema = appSchema({
|
|||
ChannelMembershipSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
CustomProfileFieldSchema,
|
||||
CustomProfileAttributeSchema,
|
||||
DraftSchema,
|
||||
FileSchema,
|
||||
GroupSchema,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {tableSchema} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
const {CUSTOM_PROFILE_ATTRIBUTE} = MM_TABLES.SERVER;
|
||||
|
||||
export default tableSchema({
|
||||
name: CUSTOM_PROFILE_ATTRIBUTE,
|
||||
columns: [
|
||||
{name: 'field_id', type: 'string', isIndexed: true},
|
||||
{name: 'user_id', type: 'string', isIndexed: true},
|
||||
{name: 'value', type: 'string'},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {tableSchema} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
const {CUSTOM_PROFILE_FIELD} = MM_TABLES.SERVER;
|
||||
|
||||
export default tableSchema({
|
||||
name: CUSTOM_PROFILE_FIELD,
|
||||
columns: [
|
||||
{name: 'group_id', type: 'string'},
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'target_id', type: 'string'},
|
||||
{name: 'target_type', type: 'string'},
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'attrs', type: 'string', isOptional: true},
|
||||
],
|
||||
});
|
||||
|
|
@ -8,6 +8,8 @@ export {default as ChannelBookmarkSchema} from './channel_bookmark';
|
|||
export {default as ChannelInfoSchema} from './channel_info';
|
||||
export {default as ChannelMembershipSchema} from './channel_membership';
|
||||
export {default as CustomEmojiSchema} from './custom_emoji';
|
||||
export {default as CustomProfileFieldSchema} from './custom_profile_field';
|
||||
export {default as CustomProfileAttributeSchema} from './custom_profile_attribute';
|
||||
export {default as DraftSchema} from './draft';
|
||||
export {default as FileSchema} from './file';
|
||||
export {default as GroupSchema} from './group';
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ const {
|
|||
CHANNEL_MEMBERSHIP,
|
||||
CONFIG,
|
||||
CUSTOM_EMOJI,
|
||||
CUSTOM_PROFILE_FIELD,
|
||||
CUSTOM_PROFILE_ATTRIBUTE,
|
||||
DRAFT,
|
||||
FILE,
|
||||
GROUP,
|
||||
|
|
@ -46,7 +48,7 @@ const {
|
|||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 7,
|
||||
version: 8,
|
||||
unsafeSql: undefined,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
|
|
@ -208,6 +210,46 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
},
|
||||
columnArray: [{name: 'name', type: 'string', isIndexed: true}],
|
||||
},
|
||||
[CUSTOM_PROFILE_FIELD]: {
|
||||
name: CUSTOM_PROFILE_FIELD,
|
||||
unsafeSql: undefined,
|
||||
columns: {
|
||||
group_id: {name: 'group_id', type: 'string'},
|
||||
name: {name: 'name', type: 'string'},
|
||||
type: {name: 'type', type: 'string'},
|
||||
target_id: {name: 'target_id', type: 'string'},
|
||||
target_type: {name: 'target_type', type: 'string'},
|
||||
create_at: {name: 'create_at', type: 'number'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
delete_at: {name: 'delete_at', type: 'number'},
|
||||
attrs: {name: 'attrs', type: 'string', isOptional: true},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'group_id', type: 'string'},
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'target_id', type: 'string'},
|
||||
{name: 'target_type', type: 'string'},
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'attrs', type: 'string', isOptional: true},
|
||||
],
|
||||
},
|
||||
[CUSTOM_PROFILE_ATTRIBUTE]: {
|
||||
name: CUSTOM_PROFILE_ATTRIBUTE,
|
||||
unsafeSql: undefined,
|
||||
columns: {
|
||||
field_id: {name: 'field_id', type: 'string', isIndexed: true},
|
||||
user_id: {name: 'user_id', type: 'string', isIndexed: true},
|
||||
value: {name: 'value', type: 'string'},
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'field_id', type: 'string', isIndexed: true},
|
||||
{name: 'user_id', type: 'string', isIndexed: true},
|
||||
{name: 'value', type: 'string'},
|
||||
],
|
||||
},
|
||||
[MY_CHANNEL]: {
|
||||
name: MY_CHANNEL,
|
||||
unsafeSql: undefined,
|
||||
|
|
|
|||
790
app/queries/servers/custom_profile.test.ts
Normal file
790
app/queries/servers/custom_profile.test.ts
Normal file
|
|
@ -0,0 +1,790 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {random} from 'lodash';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {customProfileAttributeId} from '@utils/custom_profile_attribute';
|
||||
|
||||
import {
|
||||
getCustomProfileFieldById,
|
||||
observeCustomProfileFields,
|
||||
getCustomProfileAttribute,
|
||||
observeCustomProfileAttribute,
|
||||
observeCustomProfileAttributesByUserId,
|
||||
convertProfileAttributesToCustomAttributes,
|
||||
queryCustomProfileFields,
|
||||
queryCustomProfileAttributesByUserId,
|
||||
queryCustomProfileAttributesByFieldId,
|
||||
deleteCustomProfileAttributesByFieldId,
|
||||
} from './custom_profile';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type {CustomAttribute} from '@typings/api/custom_profile_attributes';
|
||||
|
||||
describe('Custom Profile Queries', () => {
|
||||
const serverUrl = 'custom-profile.test.com';
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('getCustomProfileFieldById', () => {
|
||||
it('should return custom profile field by id', async () => {
|
||||
const fieldId = 'field1';
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [{
|
||||
id: fieldId,
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const field = await getCustomProfileFieldById(database, fieldId);
|
||||
expect(field).not.toBeUndefined();
|
||||
expect(field?.name).toBe('Test Field');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent field', async () => {
|
||||
const field = await getCustomProfileFieldById(database, 'nonexistent');
|
||||
expect(field).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeCustomProfileFields', () => {
|
||||
it('should observe all custom profile fields', (done) => {
|
||||
const errorFn = jest.fn();
|
||||
|
||||
operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
}).then(() => {
|
||||
observeCustomProfileFields(database).subscribe({
|
||||
next: (fields) => {
|
||||
expect(fields.length).toBe(2);
|
||||
expect(fields[0].name).toBe('Test Field 1');
|
||||
expect(fields[1].name).toBe('Test Field 2');
|
||||
done();
|
||||
},
|
||||
error: errorFn,
|
||||
});
|
||||
});
|
||||
expect(errorFn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomProfileAttribute', () => {
|
||||
it('should return custom profile attribute by field id and user id', async () => {
|
||||
const fieldId = 'field1';
|
||||
const userId = 'user1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [{
|
||||
id: fieldId,
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [{
|
||||
id: `${fieldId}_${userId}`,
|
||||
field_id: fieldId,
|
||||
user_id: userId,
|
||||
value: 'Test Value',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const attribute = await getCustomProfileAttribute(database, fieldId, userId);
|
||||
expect(attribute).not.toBeUndefined();
|
||||
expect(attribute?.value).toBe('Test Value');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent attribute', async () => {
|
||||
const attribute = await getCustomProfileAttribute(database, 'nonexistent', 'nonexistent');
|
||||
expect(attribute).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeCustomProfileAttribute', () => {
|
||||
it('should observe a custom profile attribute', (done) => {
|
||||
const fieldId = 'field1';
|
||||
const userId = 'user1';
|
||||
|
||||
Promise.all([
|
||||
operator.handleCustomProfileFields({
|
||||
fields: [{
|
||||
id: fieldId,
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
}),
|
||||
operator.handleCustomProfileAttributes({
|
||||
attributes: [{
|
||||
id: `${fieldId}_${userId}`,
|
||||
field_id: fieldId,
|
||||
user_id: userId,
|
||||
value: 'Test Value',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
}),
|
||||
]).then(() => {
|
||||
observeCustomProfileAttribute(database, fieldId, userId).subscribe({
|
||||
next: (attribute) => {
|
||||
expect(attribute).not.toBeUndefined();
|
||||
expect(attribute?.value).toBe('Test Value');
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent attribute', (done) => {
|
||||
observeCustomProfileAttribute(database, 'nonexistent', 'nonexistent').subscribe({
|
||||
next: (attribute) => {
|
||||
expect(attribute).toBeUndefined();
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeCustomProfileAttributesByUserId', () => {
|
||||
it('should observe all custom profile attributes for a user', (done) => {
|
||||
const userId = 'user1';
|
||||
|
||||
Promise.all([
|
||||
operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
}),
|
||||
operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `field1_${userId}`,
|
||||
field_id: 'field1',
|
||||
user_id: userId,
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `field2_${userId}`,
|
||||
field_id: 'field2',
|
||||
user_id: userId,
|
||||
value: 'Value 2',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
}),
|
||||
]).then(() => {
|
||||
observeCustomProfileAttributesByUserId(database, userId).subscribe({
|
||||
next: (attributes) => {
|
||||
expect(attributes.length).toBe(2);
|
||||
if (attributes[0].id === 'field1_user1') {
|
||||
expect(attributes[0].value).toBe('Value 1');
|
||||
expect(attributes[1].value).toBe('Value 2');
|
||||
} else if (attributes[0].id === 'field2_user1') {
|
||||
expect(attributes[0].value).toBe('Value 2');
|
||||
expect(attributes[1].value).toBe('Value 1');
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent user', (done) => {
|
||||
observeCustomProfileAttributesByUserId(database, 'nonexistent').subscribe({
|
||||
next: (attributes) => {
|
||||
expect(attributes.length).toBe(0);
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertProfileAttributesToCustomAttributes', () => {
|
||||
it('should convert profile attributes to custom attributes', async () => {
|
||||
const fieldId1 = 'field1';
|
||||
const fieldId2 = 'field2';
|
||||
const userId = 'user1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: fieldId1,
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
attrs: {sort_order: 1},
|
||||
},
|
||||
{
|
||||
id: fieldId2,
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
attrs: {sort_order: 0},
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `${fieldId1}_${userId}`,
|
||||
field_id: fieldId1,
|
||||
user_id: userId,
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `${fieldId2}_${userId}`,
|
||||
field_id: fieldId2,
|
||||
user_id: userId,
|
||||
value: 'Value 2',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const attributes = await queryCustomProfileAttributesByUserId(database, userId).fetch();
|
||||
const customAttributes = await convertProfileAttributesToCustomAttributes(database, attributes);
|
||||
|
||||
expect(customAttributes.length).toBe(2);
|
||||
if (customAttributes[0].id === fieldId1) {
|
||||
expect(customAttributes[0].id).toBe(fieldId1);
|
||||
expect(customAttributes[0].name).toBe('Test Field 1');
|
||||
expect(customAttributes[0].value).toBe('Value 1');
|
||||
expect(customAttributes[0].sort_order).toBe(1);
|
||||
|
||||
expect(customAttributes[1].id).toBe(fieldId2);
|
||||
expect(customAttributes[1].name).toBe('Test Field 2');
|
||||
expect(customAttributes[1].value).toBe('Value 2');
|
||||
expect(customAttributes[1].sort_order).toBe(0);
|
||||
} else {
|
||||
expect(customAttributes[0].id).toBe(fieldId2);
|
||||
expect(customAttributes[0].name).toBe('Test Field 2');
|
||||
expect(customAttributes[0].value).toBe('Value 2');
|
||||
expect(customAttributes[0].sort_order).toBe(0);
|
||||
|
||||
expect(customAttributes[1].id).toBe(fieldId1);
|
||||
expect(customAttributes[1].name).toBe('Test Field 1');
|
||||
expect(customAttributes[1].value).toBe('Value 1');
|
||||
expect(customAttributes[1].sort_order).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should sort custom attributes by custom sort function', async () => {
|
||||
const fieldId1 = 'field1';
|
||||
const fieldId2 = 'field2';
|
||||
const userId = 'user1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: fieldId1,
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
attrs: {sort_order: 1},
|
||||
},
|
||||
{
|
||||
id: fieldId2,
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
attrs: {sort_order: 0},
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `${fieldId1}_${userId}`,
|
||||
field_id: fieldId1,
|
||||
user_id: userId,
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `${fieldId2}_${userId}`,
|
||||
field_id: fieldId2,
|
||||
user_id: userId,
|
||||
value: 'Value 2',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const attributes = await queryCustomProfileAttributesByUserId(database, userId).fetch();
|
||||
|
||||
// Sort by sort_order in ascending order
|
||||
const customAttributes = await convertProfileAttributesToCustomAttributes(
|
||||
database,
|
||||
attributes,
|
||||
(a, b) => (a.sort_order || 0) - (b.sort_order || 0),
|
||||
);
|
||||
|
||||
expect(customAttributes.length).toBe(2);
|
||||
expect(customAttributes[0].id).toBe(fieldId2); // This has sort_order 0
|
||||
expect(customAttributes[1].id).toBe(fieldId1); // This has sort_order 1
|
||||
});
|
||||
|
||||
it('should handle empty attributes array', async () => {
|
||||
const customAttributes = await convertProfileAttributesToCustomAttributes(database, []);
|
||||
expect(customAttributes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle null attributes', async () => {
|
||||
const customAttributes = await convertProfileAttributesToCustomAttributes(database, null);
|
||||
expect(customAttributes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryCustomProfileFields', () => {
|
||||
it('should query all custom profile fields', async () => {
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const fields = await queryCustomProfileFields(database).fetch();
|
||||
expect(fields.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryCustomProfileAttributesByUserId', () => {
|
||||
it('should query all attributes for a user', async () => {
|
||||
const userId = 'user1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'Test Field 1',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
name: 'Test Field 2',
|
||||
type: 'select',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `field1_${userId}`,
|
||||
field_id: 'field1',
|
||||
user_id: userId,
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `field2_${userId}`,
|
||||
field_id: 'field2',
|
||||
user_id: userId,
|
||||
value: 'Value 2',
|
||||
},
|
||||
{
|
||||
id: 'field1_user2',
|
||||
field_id: 'field1',
|
||||
user_id: 'user2',
|
||||
value: 'Another value',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const attributes = await queryCustomProfileAttributesByUserId(database, userId).fetch();
|
||||
expect(attributes.length).toBe(2);
|
||||
if (attributes[0].id === 'field1_user1') {
|
||||
expect(attributes[0].fieldId).toBe('field1');
|
||||
expect(attributes[1].fieldId).toBe('field2');
|
||||
} else if (attributes[0].id === 'field2_user1') {
|
||||
expect(attributes[0].fieldId).toBe('field2');
|
||||
expect(attributes[1].fieldId).toBe('field1');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryCustomProfileAttributesByFieldId', () => {
|
||||
it('should query all attributes for a field', async () => {
|
||||
const fieldId = 'field1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: fieldId,
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `${fieldId}_user1`,
|
||||
field_id: fieldId,
|
||||
user_id: 'user1',
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `${fieldId}_user2`,
|
||||
field_id: fieldId,
|
||||
user_id: 'user2',
|
||||
value: 'Value 2',
|
||||
},
|
||||
{
|
||||
id: 'field2_user1',
|
||||
field_id: 'field2',
|
||||
user_id: 'user1',
|
||||
value: 'Another value',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const attributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
expect(attributes.length).toBe(2);
|
||||
if (attributes[0].id === 'field1_user1') {
|
||||
expect(attributes[0].userId).toBe('user1');
|
||||
expect(attributes[1].userId).toBe('user2');
|
||||
} else if (attributes[0].id === 'field2_user1') {
|
||||
expect(attributes[0].userId).toBe('user2');
|
||||
expect(attributes[1].userId).toBe('user1');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCustomProfileAttributesByFieldId', () => {
|
||||
it('should delete all attributes for a field', async () => {
|
||||
const fieldId = 'field1';
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: fieldId,
|
||||
name: 'Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes: [
|
||||
{
|
||||
id: `${fieldId}_user1`,
|
||||
field_id: fieldId,
|
||||
user_id: 'user1',
|
||||
value: 'Value 1',
|
||||
},
|
||||
{
|
||||
id: `${fieldId}_user2`,
|
||||
field_id: fieldId,
|
||||
user_id: 'user2',
|
||||
value: 'Value 2',
|
||||
},
|
||||
{
|
||||
id: 'field2_user1',
|
||||
field_id: 'field2',
|
||||
user_id: 'user1',
|
||||
value: 'Another value',
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Check initial state
|
||||
let field1Attributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
let field2Attributes = await queryCustomProfileAttributesByFieldId(database, 'field2').fetch();
|
||||
expect(field1Attributes.length).toBe(2);
|
||||
expect(field2Attributes.length).toBe(1);
|
||||
|
||||
// Delete attributes for field1
|
||||
await deleteCustomProfileAttributesByFieldId(database, fieldId);
|
||||
|
||||
// Verify deletion
|
||||
field1Attributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
field2Attributes = await queryCustomProfileAttributesByFieldId(database, 'field2').fetch();
|
||||
expect(field1Attributes.length).toBe(0);
|
||||
expect(field2Attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should delete attributes with custom batch size', async () => {
|
||||
const fieldId = 'field3';
|
||||
const attributeCount = 5;
|
||||
|
||||
// Create a field
|
||||
await operator.handleCustomProfileFields({
|
||||
fields: [
|
||||
{
|
||||
id: fieldId,
|
||||
name: 'Batch Test Field',
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Create multiple attributes for testing batch deletion
|
||||
const attributes = Array.from({length: attributeCount}, (_, i) => ({
|
||||
id: `${fieldId}_user${i}`,
|
||||
field_id: fieldId,
|
||||
user_id: `user${i}`,
|
||||
value: `Value ${i}`,
|
||||
}));
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Verify initial state
|
||||
let fieldAttributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
expect(fieldAttributes.length).toBe(attributeCount);
|
||||
|
||||
// Delete with custom batch size (smaller than total count)
|
||||
await deleteCustomProfileAttributesByFieldId(database, fieldId, 2);
|
||||
|
||||
// Verify all attributes were deleted despite the small batch size
|
||||
fieldAttributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
expect(fieldAttributes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle case when no attributes exist', async () => {
|
||||
const nonExistentFieldId = 'nonexistent_field';
|
||||
|
||||
// Try to delete attributes for a field that doesn't exist
|
||||
await deleteCustomProfileAttributesByFieldId(database, nonExistentFieldId);
|
||||
|
||||
// Function should complete without errors
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Tests', () => {
|
||||
it('should profile convertProfileAttributesToCustomAttributes performance', async () => {
|
||||
jest.setTimeout(30000); // Increase timeout for this test
|
||||
|
||||
// Create a larger dataset to test performance
|
||||
const fieldCount = 200;
|
||||
const userCount = 5;
|
||||
|
||||
// Create fields
|
||||
const fields = Array.from({length: fieldCount}, (_, i) => ({
|
||||
id: `field${i}`,
|
||||
name: `Test Field ${i}`,
|
||||
type: 'text',
|
||||
delete_at: 0,
|
||||
group_id: '',
|
||||
target_id: '',
|
||||
target_type: 'user',
|
||||
create_at: 1000,
|
||||
update_at: 1000,
|
||||
attrs: {sort_order: i + random(0, 1000)},
|
||||
}));
|
||||
|
||||
await operator.handleCustomProfileFields({
|
||||
fields,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Create attributes (10 attributes per user)
|
||||
const attributes = [];
|
||||
for (let u = 0; u < userCount; u++) {
|
||||
const userId = `user${u}`;
|
||||
for (let f = 0; f < fieldCount; f++) {
|
||||
const fieldId = `field${f}`;
|
||||
attributes.push({
|
||||
id: customProfileAttributeId(fieldId, userId),
|
||||
field_id: fieldId,
|
||||
user_id: userId,
|
||||
value: `Value for user ${u} field ${f}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await operator.handleCustomProfileAttributes({
|
||||
attributes,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
// Profile the function for one specific user
|
||||
const userId = 'user0';
|
||||
const userAttributes = await queryCustomProfileAttributesByUserId(database, userId).fetch();
|
||||
|
||||
console.log(`Testing conversion of ${userAttributes.length} attributes`);
|
||||
|
||||
// Run the function once to get performance data
|
||||
const sortFn = (a: CustomAttribute, b: CustomAttribute) => (a.sort_order || 0) - (b.sort_order || 0);
|
||||
|
||||
const startTime = performance.now();
|
||||
await convertProfileAttributesToCustomAttributes(database, userAttributes, sortFn);
|
||||
const endTime = performance.now();
|
||||
console.log(`Time with sorting: ${(endTime - startTime).toFixed(2)}ms`);
|
||||
|
||||
expect(true).toBe(true); // No assertions needed for profiling
|
||||
});
|
||||
});
|
||||
});
|
||||
192
app/queries/servers/custom_profile.ts
Normal file
192
app/queries/servers/custom_profile.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, type Database} from '@nozbe/watermelondb';
|
||||
import {of as of$, type Observable} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {customProfileAttributeId} from '@utils/custom_profile_attribute';
|
||||
|
||||
import type {CustomAttribute} from '@typings/api/custom_profile_attributes';
|
||||
import type CustomProfileAttributeModel from 'app/database/models/server/custom_profile_attribute';
|
||||
import type CustomProfileFieldModel from 'app/database/models/server/custom_profile_field';
|
||||
|
||||
const {CUSTOM_PROFILE_FIELD, CUSTOM_PROFILE_ATTRIBUTE} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* Get a custom profile field by ID
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The field ID
|
||||
* @returns The custom profile field or undefined
|
||||
*/
|
||||
export const getCustomProfileFieldById = async (database: Database, fieldId: string) => {
|
||||
try {
|
||||
const fieldRecord = await database.get<CustomProfileFieldModel>(CUSTOM_PROFILE_FIELD).find(fieldId);
|
||||
return fieldRecord;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe a custom profile field
|
||||
* @param database - The database instance
|
||||
* @returns Observable of the custom profile field
|
||||
*/
|
||||
export const observeCustomProfileFields = (database: Database) => {
|
||||
return queryCustomProfileFields(database).observeWithColumns(['update_at', 'delete_at']).pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a custom profile attribute by field ID and user ID
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The field ID
|
||||
* @param userId - The user ID
|
||||
* @returns The custom profile attribute or undefined
|
||||
*/
|
||||
export const getCustomProfileAttribute = async (database: Database, fieldId: string, userId: string) => {
|
||||
try {
|
||||
const attributeRecord = await database.get<CustomProfileAttributeModel>(CUSTOM_PROFILE_ATTRIBUTE).query(
|
||||
Q.where('id', customProfileAttributeId(fieldId, userId)),
|
||||
Q.take(1),
|
||||
).fetch();
|
||||
return attributeRecord[0];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe a custom profile attribute
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The field ID
|
||||
* @param userId - The user ID
|
||||
* @returns Observable of the custom profile attribute
|
||||
*/
|
||||
export const observeCustomProfileAttribute = (database: Database, fieldId: string, userId: string) => {
|
||||
return database.get<CustomProfileAttributeModel>(CUSTOM_PROFILE_ATTRIBUTE).query(
|
||||
Q.where('id', customProfileAttributeId(fieldId, userId)),
|
||||
Q.take(1),
|
||||
).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(undefined))),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe all custom profile attributes for a user
|
||||
* @param database - The database instance
|
||||
* @param userId - The user ID
|
||||
* @returns Observable of the custom profile attributes
|
||||
*/
|
||||
export const observeCustomProfileAttributesByUserId = (database: Database, userId: string): Observable<CustomProfileAttributeModel[]> => {
|
||||
return queryCustomProfileAttributesByUserId(database, userId).observeWithColumns(['value']).pipe(distinctUntilChanged());
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert custom profile attributes to the UI-ready CustomAttribute format
|
||||
* @param database - The database instance
|
||||
* @param attributes - Array of custom profile attribute models
|
||||
* @returns Promise resolving to array of formatted CustomAttribute objects
|
||||
*/
|
||||
export const convertProfileAttributesToCustomAttributes = async (
|
||||
database: Database,
|
||||
attributes: CustomProfileAttributeModel[] | null | undefined,
|
||||
sortFn?: (a: CustomAttribute, b: CustomAttribute) => number,
|
||||
): Promise<CustomAttribute[]> => {
|
||||
if (!attributes?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We need to fetch field details for names and sorting
|
||||
const fieldIds = attributes.map((attr) => attr.fieldId);
|
||||
const fieldsQuery = await database.get<CustomProfileFieldModel>(CUSTOM_PROFILE_FIELD).query(
|
||||
Q.where('id', Q.oneOf(fieldIds)),
|
||||
).fetch();
|
||||
|
||||
// Create a map of field IDs to names
|
||||
const fieldsMap = new Map();
|
||||
fieldsQuery.forEach((field) => {
|
||||
fieldsMap.set(field.id, {
|
||||
name: field.name,
|
||||
sort_order: field.attrs?.sort_order || 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Convert DB attributes to CustomAttribute format
|
||||
const customAttrs = attributes.map((attr) => ({
|
||||
id: attr.fieldId,
|
||||
name: fieldsMap.get(attr.fieldId)?.name || attr.fieldId,
|
||||
value: attr.value,
|
||||
sort_order: fieldsMap.get(attr.fieldId)?.sort_order || 0,
|
||||
}));
|
||||
|
||||
// Sort if a sort function is provided
|
||||
return sortFn ? customAttrs.sort(sortFn) : customAttrs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Query to fetch all custom profile fields
|
||||
* @param database - The database instance
|
||||
* @returns Query for custom profile fields
|
||||
*/
|
||||
export const queryCustomProfileFields = (database: Database) => {
|
||||
return database.get<CustomProfileFieldModel>(CUSTOM_PROFILE_FIELD).query(Q.where('delete_at', 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Query to fetch custom profile attributes for a user
|
||||
* @param database - The database instance
|
||||
* @param userId - The user ID
|
||||
* @returns Query for custom profile attributes
|
||||
*/
|
||||
export const queryCustomProfileAttributesByUserId = (database: Database, userId: string) => {
|
||||
return database.get<CustomProfileAttributeModel>(CUSTOM_PROFILE_ATTRIBUTE).query(
|
||||
Q.where('user_id', userId),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Query to fetch custom profile attributes by field ID
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The field ID
|
||||
* @returns Query for custom profile attributes
|
||||
*/
|
||||
export const queryCustomProfileAttributesByFieldId = (database: Database, fieldId: string) => {
|
||||
return database.get<CustomProfileAttributeModel>(CUSTOM_PROFILE_ATTRIBUTE).query(
|
||||
Q.where('field_id', fieldId),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete custom profile attributes by field ID with batching support
|
||||
* @param database - The database instance
|
||||
* @param fieldId - The field ID
|
||||
* @param batchSize - Number of records to delete in each batch (defaults to 100)
|
||||
* @returns Promise that resolves when the deletion is complete
|
||||
*/
|
||||
export const deleteCustomProfileAttributesByFieldId = async (database: Database, fieldId: string, batchSize = 100) => {
|
||||
const attributes = await queryCustomProfileAttributesByFieldId(database, fieldId).fetch();
|
||||
|
||||
if (!attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process attributes in batches to avoid performance issues with large datasets
|
||||
const promises = [];
|
||||
for (let i = 0; i < attributes.length; i += batchSize) {
|
||||
const batch = attributes.slice(i, i + batchSize);
|
||||
const preparedModels = batch.map((attribute) => attribute.prepareDestroyPermanently());
|
||||
|
||||
const batchPromise = database.write(async () => {
|
||||
await database.batch(...preparedModels);
|
||||
}, `deleteCustomProfileAttributesByFieldId:${fieldId}:batch:${i}`);
|
||||
|
||||
promises.push(batchPromise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
4
app/utils/custom_profile_attribute.ts
Normal file
4
app/utils/custom_profile_attribute.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const customProfileAttributeId = (fieldId: string, userId: string) => `${fieldId}-${userId}`;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Server Database - Schema Version 4
|
||||
# Server Database - Schema Version 5
|
||||
# Please bump the version by 1, any time the schema changes.
|
||||
# Also, include the migration plan under app/database/migration/server,
|
||||
# update all models, relationships and types.
|
||||
|
|
@ -65,6 +65,28 @@ id PK string # auto-generated
|
|||
name string
|
||||
|
||||
|
||||
CustomProfileField
|
||||
-
|
||||
id PK string # server-generated
|
||||
group_id string
|
||||
name string
|
||||
type string
|
||||
target_id string
|
||||
target_type string
|
||||
create_at number
|
||||
update_at number
|
||||
delete_at number
|
||||
attrs string NULL # stringified JSON
|
||||
|
||||
|
||||
CustomProfileAttribute
|
||||
-
|
||||
id PK string # composition ID User.id-CustomProfileField.id
|
||||
field_id string INDEX FK >- CustomProfileField.id
|
||||
user_id string INDEX FK >- User.id
|
||||
value string
|
||||
|
||||
|
||||
Draft
|
||||
-
|
||||
id PK string # auto-generated
|
||||
|
|
|
|||
|
|
@ -549,6 +549,7 @@ class TestHelperSingleton {
|
|||
|
||||
channelsCreated: this.fakeQuery([]),
|
||||
channels: this.fakeQuery([]),
|
||||
customProfileAttributes: this.fakeQuery([]),
|
||||
posts: this.fakeQuery([]),
|
||||
preferences: this.fakeQuery([]),
|
||||
reactions: this.fakeQuery([]),
|
||||
|
|
|
|||
23
types/api/custom_profile_attributes.d.ts
vendored
23
types/api/custom_profile_attributes.d.ts
vendored
|
|
@ -37,9 +37,28 @@ type CustomProfileField = {
|
|||
|
||||
/**
|
||||
* CustomProfileAttributeSimple
|
||||
* @description simpler type to display a field id with its value.
|
||||
* @description Type representing a custom profile attribute with its field ID, user ID, and value.
|
||||
**/
|
||||
type CustomProfileAttributeSimple = {
|
||||
type CustomProfileAttribute = {
|
||||
|
||||
/** ID of the custom profile attribute */
|
||||
id: string;
|
||||
|
||||
/** ID of the custom profile field this attribute is for */
|
||||
field_id: string;
|
||||
|
||||
/** ID of the user this attribute belongs to */
|
||||
user_id: string;
|
||||
|
||||
/** Value of the attribute */
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserCustomProfileAttributeSimple
|
||||
* @description simpler type to display a field id with its value, when we already know it all belongs to the same user
|
||||
**/
|
||||
type UserCustomProfileAttributeSimple = {
|
||||
[field_id: string]: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {Database} from '@nozbe/watermelondb';
|
|||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type {Clause} from '@nozbe/watermelondb/QueryDescription';
|
||||
import type {Class} from '@nozbe/watermelondb/types';
|
||||
import type {CustomProfileField, CustomProfileAttribute} from '@typings/api/custom_profile_attributes';
|
||||
import type System from '@typings/database/models/servers/system';
|
||||
|
||||
export type WithDatabaseArgs = { database: Database }
|
||||
|
|
@ -294,6 +295,14 @@ export type HandleDraftArgs = PrepareOnly & {
|
|||
drafts?: Draft[];
|
||||
};
|
||||
|
||||
export type HandleCustomProfileFieldsArgs = PrepareOnly & {
|
||||
fields?: CustomProfileField[];
|
||||
};
|
||||
|
||||
export type HandleCustomProfileAttributesArgs = PrepareOnly & {
|
||||
attributes?: CustomProfileAttribute[];
|
||||
};
|
||||
|
||||
export type LoginArgs = {
|
||||
config: Partial<ClientConfig>;
|
||||
ldapOnly?: boolean;
|
||||
|
|
|
|||
32
types/database/models/servers/custom_profile_attribute.ts
Normal file
32
types/database/models/servers/custom_profile_attribute.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type CustomProfileFieldModel from './custom_profile_field';
|
||||
import type UserModel from './user';
|
||||
import type {Relation, Model} from '@nozbe/watermelondb';
|
||||
import type {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
declare class CustomProfileAttributeModel extends Model {
|
||||
/** table (name) : CustomProfileAttribute */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** field_id : The identifier of the custom profile field this attribute is for */
|
||||
fieldId: string;
|
||||
|
||||
/** user_id : The identifier of the user this attribute belongs to */
|
||||
userId: string;
|
||||
|
||||
/** value : The value of the custom profile attribute */
|
||||
value: string;
|
||||
|
||||
/** Relation to the custom profile field */
|
||||
field: Relation<CustomProfileFieldModel>;
|
||||
|
||||
/** Relation to the user */
|
||||
user: Relation<UserModel>;
|
||||
}
|
||||
|
||||
export default CustomProfileAttributeModel;
|
||||
48
types/database/models/servers/custom_profile_field.ts
Normal file
48
types/database/models/servers/custom_profile_field.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type CustomProfileAttributeModel from './custom_profile_attribute';
|
||||
import type {Model, Query} from '@nozbe/watermelondb';
|
||||
import type {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
export type CustomProfileFieldAttrs = {
|
||||
sort_order?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
declare class CustomProfileFieldModel extends Model {
|
||||
static table: string;
|
||||
static associations: Associations;
|
||||
|
||||
/** group_id : The identifier of the group this field belongs to */
|
||||
groupId: string;
|
||||
|
||||
/** name : The name of the custom profile field */
|
||||
name: string;
|
||||
|
||||
/** type : The type of values accepted (e.g., 'text') */
|
||||
type: string;
|
||||
|
||||
/** target_id : The id of the target element (empty if system property) */
|
||||
targetId: string;
|
||||
|
||||
/** target_type : The type of element this is assigned to (e.g., 'user', 'post', 'card') */
|
||||
targetType: string;
|
||||
|
||||
/** create_at : The timestamp when this field was created */
|
||||
createAt: number;
|
||||
|
||||
/** update_at : The timestamp when this field was last updated */
|
||||
updateAt: number;
|
||||
|
||||
/** delete_at : The timestamp when this field was deleted (0 if not deleted) */
|
||||
deleteAt: number;
|
||||
|
||||
/** attrs : Any extra properties of the field */
|
||||
attrs: CustomProfileFieldAttrs | null;
|
||||
|
||||
/** customProfileAttributes : All the custom profile attributes for this field */
|
||||
customProfileAttributes: Query<CustomProfileAttributeModel>;
|
||||
}
|
||||
|
||||
export default CustomProfileFieldModel;
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import type ChannelModel from './channel';
|
||||
import type ChannelMembershipModel from './channel_membership';
|
||||
import type CustomProfileAttributeModel from './custom_profile_attribute';
|
||||
import type PostModel from './post';
|
||||
import type PreferenceModel from './preference';
|
||||
import type ReactionModel from './reaction';
|
||||
|
|
@ -121,6 +122,9 @@ declare class UserModel extends Model {
|
|||
|
||||
/** termsOfServiceCreateAt : The last time the user accepted the terms of service */
|
||||
termsOfServiceCreateAt: number;
|
||||
|
||||
/** customProfileAttributes : All the custom profile attributes for this user */
|
||||
customProfileAttributes: Query<CustomProfileAttributeModel> | undefined;
|
||||
}
|
||||
|
||||
export default UserModel;
|
||||
|
|
|
|||
2
types/database/raw_values.d.ts
vendored
2
types/database/raw_values.d.ts
vendored
|
|
@ -137,4 +137,6 @@ type RawValue =
|
|||
| TeamThreadsSync
|
||||
| UserProfile
|
||||
| SlashCommand
|
||||
| CustomProfileAttribute
|
||||
| CustomProfileField
|
||||
| Pick<ChannelMembership, 'channel_id' | 'user_id'>
|
||||
|
|
|
|||
Loading…
Reference in a new issue