[MM-65984] DB changes to handle Playbook Run Attributes (#9172)

* db changes

* temp tests

* missing mock models

* test helpers improvement

* address naming errors

* added comments to schema

* x2

* minor corrections

* Update app/products/playbooks/database/models/playbook_run_attribute_value.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* docs

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Guillermo Vayá 2025-10-12 21:48:50 +02:00 committed by GitHub
parent 597e03d7d8
commit 87395e1210
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1297 additions and 17 deletions

View file

@ -22,7 +22,7 @@ import AppDataOperator from '@database/operator/app_data_operator';
import ServerDataOperator from '@database/operator/server_data_operator';
import {schema as appSchema} from '@database/schema/app';
import {serverSchema} from '@database/schema/server';
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel} from '@playbooks/database/models';
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel} from '@playbooks/database/models';
import {deleteIOSDatabase} from '@utils/mattermost_managed';
import {urlSafeBase64Encode} from '@utils/security';
import {removeProtocol} from '@utils/url';
@ -54,7 +54,7 @@ class DatabaseManagerSingleton {
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
ScheduledPostModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel,
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel,
];
this.databaseDirectory = '';
}

View file

@ -23,7 +23,7 @@ import ServerDataOperator from '@database/operator/server_data_operator';
import {schema as appSchema} from '@database/schema/app';
import {serverSchema} from '@database/schema/server';
import {beforeUpgrade} from '@helpers/database/upgrade';
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel} from '@playbooks/database/models';
import {PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel} from '@playbooks/database/models';
import {getActiveServer, getServer, getServerByIdentifier} from '@queries/app/servers';
import {logDebug, logError} from '@utils/log';
import {deleteIOSDatabase, getIOSAppGroupDetails, renameIOSDatabase} from '@utils/mattermost_managed';
@ -50,7 +50,7 @@ class DatabaseManagerSingleton {
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
ScheduledPostModel, SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel,
PlaybookRunModel, PlaybookChecklistModel, PlaybookChecklistItemModel, PlaybookRunAttributeModel, PlaybookRunAttributeValueModel,
];
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${documentDirectory}/databases/`;

View file

@ -22,9 +22,36 @@ const {
SCHEDULED_POST,
} = MM_TABLES.SERVER;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
export default schemaMigrations({migrations: [
{
toVersion: 14,
steps: [
createTable({
name: PLAYBOOK_RUN_ATTRIBUTE,
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},
],
}),
createTable({
name: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
columns: [
{name: 'attribute_id', type: 'string', isIndexed: true},
{name: 'run_id', type: 'string', isIndexed: true},
{name: 'value', type: 'string'},
],
}),
],
},
{
toVersion: 13,
steps: [

View file

@ -3,7 +3,7 @@
import {type AppSchema, appSchema} from '@nozbe/watermelondb';
import {PlaybookRunSchema, PlaybookChecklistSchema, PlaybookChecklistItemSchema} from '@playbooks/database/schema';
import {PlaybookRunSchema, PlaybookChecklistSchema, PlaybookChecklistItemSchema, PlaybookRunAttributeSchema, PlaybookRunAttributeValueSchema} from '@playbooks/database/schema';
import {
CategorySchema,
@ -45,7 +45,7 @@ import {
} from './table_schemas';
export const serverSchema: AppSchema = appSchema({
version: 13,
version: 14,
tables: [
CategorySchema,
CategoryChannelSchema,
@ -69,6 +69,8 @@ export const serverSchema: AppSchema = appSchema({
PlaybookRunSchema,
PlaybookChecklistSchema,
PlaybookChecklistItemSchema,
PlaybookRunAttributeSchema,
PlaybookRunAttributeValueSchema,
PostInThreadSchema,
PostSchema,
PostsInChannelSchema,

View file

@ -47,12 +47,12 @@ const {
USER,
} = MM_TABLES.SERVER;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
describe('*** Test schema for SERVER database ***', () => {
it('=> The SERVER SCHEMA should strictly match', () => {
expect(serverSchema).toEqual({
version: 13,
version: 14,
unsafeSql: undefined,
tables: {
[CATEGORY]: {
@ -553,6 +553,46 @@ describe('*** Test schema for SERVER database ***', () => {
{name: 'update_at', type: 'number'},
],
},
[PLAYBOOK_RUN_ATTRIBUTE]: {
name: PLAYBOOK_RUN_ATTRIBUTE,
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},
],
},
[PLAYBOOK_RUN_ATTRIBUTE_VALUE]: {
name: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
unsafeSql: undefined,
columns: {
attribute_id: {name: 'attribute_id', type: 'string', isIndexed: true},
run_id: {name: 'run_id', type: 'string', isIndexed: true},
value: {name: 'value', type: 'string'},
},
columnArray: [
{name: 'attribute_id', type: 'string', isIndexed: true},
{name: 'run_id', type: 'string', isIndexed: true},
{name: 'value', type: 'string'},
],
},
[POSTS_IN_THREAD]: {
name: POSTS_IN_THREAD,
unsafeSql: undefined,

View file

@ -5,4 +5,6 @@ export const PLAYBOOK_TABLES = {
PLAYBOOK_RUN: 'PlaybookRun',
PLAYBOOK_CHECKLIST: 'PlaybookChecklist',
PLAYBOOK_CHECKLIST_ITEM: 'PlaybookChecklistItem',
PLAYBOOK_RUN_ATTRIBUTE: 'PlaybookRunAttribute',
PLAYBOOK_RUN_ATTRIBUTE_VALUE: 'PlaybookRunAttributeValue',
};

View file

@ -4,3 +4,5 @@
export {default as PlaybookRunModel} from './playbook_run';
export {default as PlaybookChecklistModel} from './playbook_checklist';
export {default as PlaybookChecklistItemModel} from './playbook_checklist_item';
export {default as PlaybookRunAttributeModel} from './playbook_run_attribute';
export {default as PlaybookRunAttributeValueModel} from './playbook_run_attribute_value';

View file

@ -0,0 +1,113 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import TestHelper from '@test/test_helper';
import PlaybookRunAttributeModel from './playbook_run_attribute';
import type ServerDataOperator from '@database/operator/server_data_operator';
const {PLAYBOOK_RUN_ATTRIBUTE} = PLAYBOOK_TABLES;
const SERVER_URL = `playbookRunAttributeModel.test.${Date.now()}.com`;
const applyMockData = (attribute: PlaybookRunAttributeModel, mockData: PlaybookRunAttribute, includeAttrs = true) => {
attribute._raw.id = mockData.id;
attribute.groupId = mockData.group_id;
attribute.name = mockData.name;
attribute.type = mockData.type;
attribute.targetId = mockData.target_id;
attribute.targetType = mockData.target_type;
attribute.createAt = mockData.create_at;
attribute.updateAt = mockData.update_at;
attribute.deleteAt = mockData.delete_at;
if (includeAttrs) {
attribute.attrs = mockData.attrs || '{"placeholder": "Enter value"}';
}
};
describe('PlaybookRunAttributeModel', () => {
let operator: ServerDataOperator;
let playbook_run_attribute: PlaybookRunAttributeModel;
beforeEach(async () => {
await DatabaseManager.init([SERVER_URL]);
operator = DatabaseManager.serverDatabases[SERVER_URL]!.operator;
const {database} = operator;
await database.write(async () => {
playbook_run_attribute = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunAttributeModel) => {
applyMockData(attribute, TestHelper.createPlaybookRunAttribute('test', 0));
});
});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(SERVER_URL);
});
it('=> should match model', () => {
expect(playbook_run_attribute).toBeDefined();
expect(playbook_run_attribute.id).toBe('test-attribute_0');
expect(playbook_run_attribute.groupId).toBe('group_1');
expect(playbook_run_attribute.name).toBe('Attribute 1');
expect(playbook_run_attribute.type).toBe('text');
expect(playbook_run_attribute.targetId).toBe('test');
expect(playbook_run_attribute.targetType).toBe('playbook_run');
expect(playbook_run_attribute.createAt).toBeGreaterThan(0);
expect(playbook_run_attribute.updateAt).toBeGreaterThan(0);
expect(playbook_run_attribute.deleteAt).toBe(0);
expect(playbook_run_attribute.attrs).toBe('{"placeholder": "Enter value"}');
});
it('=> should have the correct table name', () => {
expect(PlaybookRunAttributeModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
});
it('=> should handle optional attrs field', async () => {
let attributeWithoutAttrs: PlaybookRunAttributeModel;
const {database} = operator;
await database.write(async () => {
attributeWithoutAttrs = await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attribute: PlaybookRunAttributeModel) => {
applyMockData(attribute, TestHelper.createPlaybookRunAttribute('test', 1), false);
});
});
expect(attributeWithoutAttrs!).toBeDefined();
expect(attributeWithoutAttrs!.id).toBe('test-attribute_1');
expect(attributeWithoutAttrs!.attrs).toBeNull();
});
it('=> should allow updating fields', async () => {
const {database} = operator;
await database.write(async () => {
await playbook_run_attribute.update((attribute: PlaybookRunAttributeModel) => {
attribute.name = 'Updated Attribute Name';
attribute.type = 'number';
attribute.updateAt = 1620000005000;
attribute.attrs = '{"min": 0, "max": 100}';
});
});
expect(playbook_run_attribute.name).toBe('Updated Attribute Name');
expect(playbook_run_attribute.type).toBe('number');
expect(playbook_run_attribute.updateAt).toBe(1620000005000);
expect(playbook_run_attribute.attrs).toBe('{"min": 0, "max": 100}');
});
it('=> should handle deleteAt field correctly', async () => {
const {database} = operator;
await database.write(async () => {
await playbook_run_attribute.update((attribute: PlaybookRunAttributeModel) => {
attribute.deleteAt = 1620000010000;
});
});
expect(playbook_run_attribute.deleteAt).toBe(1620000010000);
});
});

View file

@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {field} from '@nozbe/watermelondb/decorators';
import Model, {type Associations} from '@nozbe/watermelondb/Model';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import type PlaybookRunAttributeModelInterface from '@playbooks/types/database/models/playbook_run_attribute';
const {PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
/**
* The model represents a playbook run attribute definition in the Mattermost app.
*/
export default class PlaybookRunAttributeModel extends Model implements PlaybookRunAttributeModelInterface {
/** table (name) : PlaybookRunAttribute */
static table = PLAYBOOK_RUN_ATTRIBUTE;
/** associations : Describes every relationship to this table. */
static associations: Associations = {
[PLAYBOOK_RUN_ATTRIBUTE_VALUE]: {type: 'has_many', foreignKey: 'attribute_id'},
};
/** groupId : The group ID of the attribute */
@field('group_id') groupId!: string;
/** name : The name of the attribute */
@field('name') name!: string;
/** type : The type of the attribute */
@field('type') type!: string;
/** targetId : The target ID of the attribute */
@field('target_id') targetId!: string;
/** targetType : The target type of the attribute */
@field('target_type') targetType!: string;
/** createAt : The timestamp of when the attribute was created */
@field('create_at') createAt!: number;
/** updateAt : The timestamp of when the attribute was last updated */
@field('update_at') updateAt!: number;
/** deleteAt : The timestamp of when the attribute was deleted */
@field('delete_at') deleteAt!: number;
/** attrs : Additional attributes for the attribute (JSON string) */
@field('attrs') attrs?: string;
}

View file

@ -0,0 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import TestHelper from '@test/test_helper';
import PlaybookRunModel from './playbook_run';
import PlaybookRunAttributeModel from './playbook_run_attribute';
import PlaybookRunAttributeValueModel from './playbook_run_attribute_value';
import type ServerDataOperator from '@database/operator/server_data_operator';
const {PLAYBOOK_RUN_ATTRIBUTE_VALUE, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN} = PLAYBOOK_TABLES;
const SERVER_URL = `playbookRunAttributeValueModel.test.${Date.now()}.com`;
const applyRunData = (run: PlaybookRunModel, mockData: PlaybookRun) => {
run._raw.id = mockData.id;
run.playbookId = mockData.playbook_id;
run.postId = mockData.post_id ?? null;
run.ownerUserId = mockData.owner_user_id;
run.teamId = mockData.team_id;
run.channelId = mockData.channel_id;
run.createAt = mockData.create_at;
run.endAt = mockData.end_at;
run.name = mockData.name;
run.description = mockData.description;
run.isActive = mockData.is_active;
run.activeStage = mockData.active_stage;
run.activeStageTitle = mockData.active_stage_title;
run.participantIds = mockData.participant_ids;
run.summary = mockData.summary;
run.currentStatus = mockData.current_status;
run.lastStatusUpdateAt = mockData.last_status_update_at;
run.retrospectiveEnabled = mockData.retrospective_enabled;
run.retrospective = mockData.retrospective;
run.retrospectivePublishedAt = mockData.retrospective_published_at;
run.updateAt = mockData.update_at;
};
const applyAttributeData = (attribute: PlaybookRunAttributeModel, mockData: PlaybookRunAttribute) => {
attribute._raw.id = mockData.id;
attribute.groupId = mockData.group_id;
attribute.name = mockData.name;
attribute.type = mockData.type;
attribute.targetId = mockData.target_id;
attribute.targetType = mockData.target_type;
attribute.createAt = mockData.create_at;
attribute.updateAt = mockData.update_at;
attribute.deleteAt = mockData.delete_at;
attribute.attrs = mockData.attrs;
};
const applyAttributeValueData = (attributeValue: PlaybookRunAttributeValueModel, mockData: PlaybookRunAttributeValue) => {
attributeValue._raw.id = mockData.id;
attributeValue.attributeId = mockData.attribute_id;
attributeValue.runId = mockData.run_id;
attributeValue.value = mockData.value;
};
describe('PlaybookRunAttributeValueModel', () => {
let operator: ServerDataOperator;
let playbook_run_attribute_value: PlaybookRunAttributeValueModel;
beforeEach(async () => {
await DatabaseManager.init([SERVER_URL]);
operator = DatabaseManager.serverDatabases[SERVER_URL]!.operator;
const {database} = operator;
const mockRun = TestHelper.createPlaybookRuns(1, 0, 0)[0];
const mockAttribute = TestHelper.createPlaybookRunAttribute('test', 0);
const mockAttributeValue = TestHelper.createPlaybookRunAttributeValue(mockAttribute.id, mockRun.id, 0);
await database.write(async () => {
await database.get<PlaybookRunModel>(PLAYBOOK_RUN).create((run) => applyRunData(run, mockRun));
await database.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((attr) => applyAttributeData(attr, mockAttribute));
playbook_run_attribute_value = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attrVal) => applyAttributeValueData(attrVal, mockAttributeValue));
});
});
afterEach(async () => {
await DatabaseManager.destroyServerDatabase(SERVER_URL);
});
it('=> should match model', () => {
expect(playbook_run_attribute_value).toBeDefined();
expect(playbook_run_attribute_value.id).toBe('playbook_run_0-test-attribute_0-value_0');
expect(playbook_run_attribute_value.attributeId).toBe('test-attribute_0');
expect(playbook_run_attribute_value.runId).toBe('playbook_run_0');
expect(playbook_run_attribute_value.value).toBe('Value 1');
});
it('=> should have the correct table name', () => {
expect(PlaybookRunAttributeValueModel.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
});
it('=> should have correct associations defined', () => {
expect(PlaybookRunAttributeValueModel.associations).toBeDefined();
expect(PlaybookRunAttributeValueModel.associations[PLAYBOOK_RUN_ATTRIBUTE]).toEqual({
type: 'belongs_to',
key: 'attribute_id',
});
expect(PlaybookRunAttributeValueModel.associations[PLAYBOOK_RUN]).toEqual({
type: 'belongs_to',
key: 'run_id',
});
});
it('=> should have attribute relation', async () => {
const attributeRelation = playbook_run_attribute_value.attribute;
expect(attributeRelation).toBeDefined();
const relatedAttribute = await attributeRelation.fetch();
expect(relatedAttribute).toBeDefined();
expect(relatedAttribute!.id).toBe('test-attribute_0');
expect(relatedAttribute!.name).toBe('Attribute 1');
});
it('=> should have run relation', async () => {
const runRelation = playbook_run_attribute_value.run;
expect(runRelation).toBeDefined();
const relatedRun = await runRelation.fetch();
expect(relatedRun).toBeDefined();
expect(relatedRun!.id).toBe('playbook_run_0');
expect(relatedRun!.name).toBe('Playbook Run 1');
});
it('=> should allow updating value', async () => {
const {database} = operator;
await database.write(async () => {
await playbook_run_attribute_value.update((attributeValue: PlaybookRunAttributeValueModel) => {
attributeValue.value = 'Updated Test Value';
});
});
expect(playbook_run_attribute_value.value).toBe('Updated Test Value');
});
it('=> should handle empty value', async () => {
let attributeValueWithEmptyValue: PlaybookRunAttributeValueModel;
const {database} = operator;
await database.write(async () => {
const mockData = TestHelper.createPlaybookRunAttributeValue('test-attribute_0', 'playbook_run_0', 1);
attributeValueWithEmptyValue = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunAttributeValueModel) => {
applyAttributeValueData(attributeValue, mockData);
attributeValue.value = '';
});
});
expect(attributeValueWithEmptyValue!).toBeDefined();
expect(attributeValueWithEmptyValue!.value).toBe('');
});
it('=> should handle long text values', async () => {
const longValue = 'This is a very long text value that might be used to store detailed information about the playbook run attribute. It could contain multiple sentences and various types of content.';
let attributeValueWithLongText: PlaybookRunAttributeValueModel;
const {database} = operator;
await database.write(async () => {
const mockData = TestHelper.createPlaybookRunAttributeValue('test-attribute_0', 'playbook_run_0', 2);
attributeValueWithLongText = await database.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((attributeValue: PlaybookRunAttributeValueModel) => {
applyAttributeValueData(attributeValue, mockData);
attributeValue.value = longValue;
});
});
expect(attributeValueWithLongText!).toBeDefined();
expect(attributeValueWithLongText!.value).toBe(longValue);
});
});

View file

@ -0,0 +1,47 @@
// 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 {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import type PlaybookRunModel from './playbook_run';
import type PlaybookRunAttributeModel from './playbook_run_attribute';
import type {Relation} from '@nozbe/watermelondb';
import type PlaybookRunAttributeValueModelInterface from '@playbooks/types/database/models/playbook_run_attribute_value';
const {PLAYBOOK_RUN, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
/**
* The PlaybookRunAttributeValue model represents a playbook run attribute value in the Mattermost app.
*/
export default class PlaybookRunAttributeValueModel extends Model implements PlaybookRunAttributeValueModelInterface {
/** table (name) : PlaybookRunAttributeValue */
static table = PLAYBOOK_RUN_ATTRIBUTE_VALUE;
/** associations : Describes every relationship to this table. */
static associations: Associations = {
/** A PLAYBOOK_RUN_ATTRIBUTE is associated to a PLAYBOOK_RUN_ATTRIBUTE_VALUE (relationship is 1:1) */
[PLAYBOOK_RUN_ATTRIBUTE]: {type: 'belongs_to', key: 'attribute_id'},
/** A PLAYBOOK_RUN is associated to a PLAYBOOK_RUN_ATTRIBUTE (relationship is 1:1) */
[PLAYBOOK_RUN]: {type: 'belongs_to', key: 'run_id'},
};
/** attributeId : The ID of the attribute this attribute value belongs to */
@field('attribute_id') attributeId!: string;
/** runId : The ID of the playbook run this attribute belongs to */
@field('run_id') runId!: string;
/** value : The value of the attribute */
@field('value') value!: string;
/** attribute : The attribute this attribute value belongs to */
@immutableRelation(PLAYBOOK_RUN_ATTRIBUTE, 'attribute_id') attribute!: Relation<PlaybookRunAttributeModel>;
/** run : The playbook run this attribute belongs to */
@immutableRelation(PLAYBOOK_RUN, 'run_id') run!: Relation<PlaybookRunModel>;
}

View file

@ -8,7 +8,7 @@ import TestHelper from '@test/test_helper';
import type ServerDataOperator from '@database/operator/server_data_operator';
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
describe('PlaybookHandler', () => {
let operator: ServerDataOperator;
@ -553,4 +553,154 @@ describe('PlaybookHandler', () => {
expect(checklistItemRecords.length).toBe(0);
});
});
describe('handlePlaybookRunAttribute', () => {
it('should return an empty array if attributes is undefined or empty', async () => {
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
let result = await operator.handlePlaybookRunAttribute({
attributes: undefined,
prepareRecordsOnly: true,
});
expect(result).toEqual([]);
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
result = await operator.handlePlaybookRunAttribute({
attributes: [],
prepareRecordsOnly: true,
});
expect(result).toEqual([]);
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
});
it('should process attributes correctly', async () => {
const mockAttributes = [
TestHelper.createPlaybookRunAttribute('attribute_1', 0),
TestHelper.createPlaybookRunAttribute('attribute_2', 1),
].map<PartialPlaybookRunAttribute>((attribute, index) => ({
...attribute,
run_id: 'playbook_run_1',
order: index,
}));
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
const result = await operator.handlePlaybookRunAttribute({
attributes: mockAttributes,
prepareRecordsOnly: false,
});
expect(result).toBeDefined();
expect(result.length).toBe(mockAttributes.length);
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
const {database} = operator;
const attributeRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
expect(attributeRecords.length).toBe(mockAttributes.length);
});
it('should only prepare records when prepareRecordsOnly is true', async () => {
const mockAttributes = [
TestHelper.createPlaybookRunAttribute('attribute_3', 2),
].map<PartialPlaybookRunAttribute>((attribute, index) => ({
...attribute,
run_id: 'playbook_run_1',
order: index,
}));
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
const result = await operator.handlePlaybookRunAttribute({
attributes: mockAttributes,
prepareRecordsOnly: true,
});
expect(result).toBeDefined();
expect(result.length).toBe(mockAttributes.length);
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
expect(spyOnBatchOperation).not.toHaveBeenCalled();
const {database} = operator;
const attributeRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE).query().fetch();
expect(attributeRecords.length).toBe(0);
});
});
describe('handlePlaybookRunAttributeValue', () => {
it('should return an empty array if attributeValues is undefined or empty', async () => {
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
let result = await operator.handlePlaybookRunAttributeValue({
attributeValues: undefined,
prepareRecordsOnly: true,
});
expect(result).toEqual([]);
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
result = await operator.handlePlaybookRunAttributeValue({
attributeValues: [],
prepareRecordsOnly: true,
});
expect(result).toEqual([]);
expect(spyOnPrepareRecords).not.toHaveBeenCalled();
});
it('should process attribute values correctly', async () => {
const mockAttributeValues = [
TestHelper.createPlaybookRunAttributeValue('attribute_1', 'playbook_run_1', 0),
TestHelper.createPlaybookRunAttributeValue('attribute_2', 'playbook_run_2', 1),
];
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
const result = await operator.handlePlaybookRunAttributeValue({
attributeValues: mockAttributeValues,
prepareRecordsOnly: false,
});
expect(result).toBeDefined();
expect(result.length).toBe(mockAttributeValues.length);
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
expect(spyOnBatchOperation).toHaveBeenCalledTimes(1);
const {database} = operator;
const attributeValueRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
expect(attributeValueRecords.length).toBe(mockAttributeValues.length);
});
it('should only prepare records when prepareRecordsOnly is true', async () => {
const mockAttributeValues = [
TestHelper.createPlaybookRunAttributeValue('attribute_3', 'playbook_run_3', 2),
];
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
const spyOnBatchOperation = jest.spyOn(operator, 'batchRecords');
const result = await operator.handlePlaybookRunAttributeValue({
attributeValues: mockAttributeValues,
prepareRecordsOnly: true,
});
expect(result).toBeDefined();
expect(result.length).toBe(mockAttributeValues.length);
expect(spyOnPrepareRecords).toHaveBeenCalledTimes(1);
expect(spyOnBatchOperation).not.toHaveBeenCalled();
const {database} = operator;
const attributeValueRecords = await database.get(PLAYBOOK_RUN_ATTRIBUTE_VALUE).query().fetch();
expect(attributeValueRecords.length).toBe(0);
});
});
});

View file

@ -8,7 +8,7 @@ import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import {logWarning} from '@utils/log';
import {shouldHandlePlaybookChecklistItemRecord, shouldHandlePlaybookChecklistRecord, shouldUpdatePlaybookRunRecord} from '../comparators';
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord} from '../transformers';
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunAttributeRecord, transformPlaybookRunAttributeValueRecord} from '../transformers';
import type ServerDataOperatorBase from '@database/operator/server_data_operator/handlers';
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
@ -34,12 +34,24 @@ type HandlePlaybookChecklistItemArgs = {
items?: PartialChecklistItem[];
}
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES;
type HandlePlaybookRunAttributeArgs = {
prepareRecordsOnly: boolean;
attributes?: PartialPlaybookRunAttribute[];
}
type HandlePlaybookRunAttributeValueArgs = {
prepareRecordsOnly: boolean;
attributeValues?: PartialPlaybookRunAttributeValue[];
}
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
export interface PlaybookHandlerMix {
handlePlaybookRun: (args: HandlePlaybookRunArgs) => Promise<Model[]>;
handlePlaybookChecklist: (args: HandlePlaybookChecklistArgs) => Promise<Model[]>;
handlePlaybookChecklistItem: (args: HandlePlaybookChecklistItemArgs) => Promise<PlaybookChecklistItemModel[]>;
handlePlaybookRunAttribute: (args: HandlePlaybookRunAttributeArgs) => Promise<Model[]>;
handlePlaybookRunAttributeValue: (args: HandlePlaybookRunAttributeValueArgs) => Promise<Model[]>;
}
const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superclass: TBase) => class extends superclass {
@ -274,6 +286,58 @@ const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supe
return records;
};
/**
* Handles the playbook run attribute records.
* @param {HandlePlaybookRunAttributeArgs} args - The arguments for handling playbook run attribute records.
* @param {boolean} args.prepareRecordsOnly - If true, only prepares the records without saving them.
* @param {PlaybookRunAttribute[]} [args.attributes] - The playbook run attribute records to handle.
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run attribute records.
*/
handlePlaybookRunAttribute = async ({attributes, prepareRecordsOnly = true}: HandlePlaybookRunAttributeArgs): Promise<Model[]> => {
if (!attributes?.length) {
logWarning(
'An empty or undefined "attributes" array has been passed to the handlePlaybookRunAttribute method',
);
return [];
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: attributes, key: 'id'});
return this.handleRecords({
fieldName: 'id',
transformer: transformPlaybookRunAttributeRecord,
createOrUpdateRawValues,
tableName: PLAYBOOK_RUN_ATTRIBUTE,
prepareRecordsOnly,
}, 'handlePlaybookRunAttribute');
};
/**
* Handles the playbook run attribute value records.
* @param {HandlePlaybookRunAttributeValueArgs} args - The arguments for handling playbook run attribute value records.
* @param {boolean} args.prepareRecordsOnly - If true, only prepares the records without saving them.
* @param {PlaybookRunAttributeValue[]} [args.attributeValues] - The playbook run attribute value records to handle.
* @returns {Promise<Model[]>} - A promise that resolves to an array of handled playbook run attribute value records.
*/
handlePlaybookRunAttributeValue = async ({attributeValues, prepareRecordsOnly = true}: HandlePlaybookRunAttributeValueArgs): Promise<Model[]> => {
if (!attributeValues?.length) {
logWarning(
'An empty or undefined "attributeValues" array has been passed to the handlePlaybookRunAttributeValue method',
);
return [];
}
const createOrUpdateRawValues = getUniqueRawsBy({raws: attributeValues, key: 'id'});
return this.handleRecords({
fieldName: 'id',
transformer: transformPlaybookRunAttributeValueRecord,
createOrUpdateRawValues,
tableName: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
prepareRecordsOnly,
}, 'handlePlaybookRunAttributeValue');
};
};
export default PlaybookHandler;

View file

@ -8,12 +8,14 @@ import {createTestConnection} from '@database/operator/utils/create_test_connect
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import {PlaybookRunModel} from '@playbooks/database/models';
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord} from '.';
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunAttributeRecord, transformPlaybookRunAttributeValueRecord} from '.';
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
const {PLAYBOOK_RUN} = PLAYBOOK_TABLES;
const {PLAYBOOK_RUN, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
describe('*** PLAYBOOK_RUN Prepare Records Test ***', () => {
it('=> transformPlaybookRunRecord: should return a record of type PlaybookRun for CREATE action', async () => {
@ -774,3 +776,287 @@ describe('*** PLAYBOOK_CHECKLIST_ITEM Prepare Records Test ***', () => {
expect(preparedRecordDefault!.conditionReason).toBe('');
});
});
describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
it('=> transformPlaybookRunAttributeRecord: should return a record of type PlaybookRunAttribute for CREATE action', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecord = await transformPlaybookRunAttributeRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'attribute_1',
group_id: 'group_1',
name: 'Test Attribute',
type: 'text',
target_id: 'target_1',
target_type: 'playbook_run',
create_at: 1620000000000,
update_at: 1620000001000,
delete_at: 0,
attrs: '{"placeholder": "Enter value"}',
},
},
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
});
it('=> transformPlaybookRunAttributeRecord: should return a record of type PlaybookRunAttribute for UPDATE action', async () => {
expect.assertions(4);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
expect(database).toBeTruthy();
let existingRecord: PlaybookRunAttributeModel | undefined;
await database!.write(async () => {
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
record._raw.id = 'attribute_2';
record.groupId = 'group_2';
record.name = 'Existing Attribute';
record.type = 'text';
record.targetId = 'target_2';
record.targetType = 'playbook_run';
record.createAt = 1620000000000;
record.updateAt = 1620000001000;
record.deleteAt = 0;
record.attrs = '{"placeholder": "Original placeholder"}';
});
});
const preparedRecord = await transformPlaybookRunAttributeRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: existingRecord,
raw: {
id: 'attribute_2',
group_id: 'group_2',
name: 'Updated Attribute',
type: 'text',
target_id: 'target_2',
target_type: 'playbook_run',
create_at: 1620000000000,
update_at: 1620000002000,
delete_at: 0,
attrs: '{"placeholder": "Updated placeholder"}',
},
},
});
await database?.write(async () => {
await database?.batch(preparedRecord);
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord!.name).toBe('Updated Attribute');
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE);
});
it('=> transformPlaybookRunAttributeRecord: should throw an error for non-create action without an existing record', async () => {
expect.assertions(2);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
expect(database).toBeTruthy();
await expect(
transformPlaybookRunAttributeRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'attribute_3',
group_id: 'group_3',
name: 'Invalid Attribute',
type: 'text',
target_id: 'target_3',
target_type: 'playbook_run',
create_at: 1620000000000,
update_at: 1620000001000,
delete_at: 0,
},
},
}),
).rejects.toThrow('Record not found for non create action');
});
it('=> transformPlaybookRunAttributeRecord: should keep most of the data if the partial attribute is empty', async () => {
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
expect(database).toBeTruthy();
let existingRecord: PlaybookRunAttributeModel | undefined;
await database!.write(async () => {
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
record._raw.id = 'attribute_2';
record.groupId = 'group_2';
record.name = 'Existing Attribute';
record.type = 'text';
record.targetId = 'target_2';
record.targetType = 'playbook_run';
record.createAt = 1620000000000;
record.updateAt = 1620000001000;
record.deleteAt = 0;
record.attrs = '{"placeholder": "Original"}';
});
});
const preparedRecord = await transformPlaybookRunAttributeRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: existingRecord,
raw: {
id: 'attribute_2',
update_at: 1620000004000,
},
},
});
await database?.write(async () => {
await database?.batch(preparedRecord);
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord.groupId).toBe('group_2');
expect(preparedRecord.name).toBe('Existing Attribute');
expect(preparedRecord.type).toBe('text');
expect(preparedRecord.targetId).toBe('target_2');
expect(preparedRecord.targetType).toBe('playbook_run');
expect(preparedRecord.createAt).toBe(1620000000000);
expect(preparedRecord.deleteAt).toBe(0);
expect(preparedRecord.attrs).toBe('{"placeholder": "Original"}');
expect(preparedRecord.updateAt).toBe(1620000004000);
});
});
describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
it('=> transformPlaybookRunAttributeValueRecord: should return a record of type PlaybookRunAttributeValue for CREATE action', async () => {
expect.assertions(3);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
expect(database).toBeTruthy();
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
action: OperationType.CREATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'attribute_value_1',
attribute_id: 'attribute_1',
run_id: 'playbook_run_1',
value: 'Test Value',
},
},
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
});
it('=> transformPlaybookRunAttributeValueRecord: should return a record of type PlaybookRunAttributeValue for UPDATE action', async () => {
expect.assertions(4);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
expect(database).toBeTruthy();
let existingRecord: PlaybookRunAttributeValueModel | undefined;
await database!.write(async () => {
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
record._raw.id = 'attribute_value_2';
record.attributeId = 'attribute_2';
record.runId = 'playbook_run_2';
record.value = 'Existing Value';
});
});
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: existingRecord,
raw: {
id: 'attribute_value_2',
attribute_id: 'attribute_2',
run_id: 'playbook_run_2',
value: 'Updated Value',
},
},
});
await database?.write(async () => {
await database?.batch(preparedRecord);
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord!.value).toBe('Updated Value');
expect(preparedRecord!.collection.table).toBe(PLAYBOOK_RUN_ATTRIBUTE_VALUE);
});
it('=> transformPlaybookRunAttributeValueRecord: should throw an error for non-create action without an existing record', async () => {
expect.assertions(2);
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
expect(database).toBeTruthy();
await expect(
transformPlaybookRunAttributeValueRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: undefined,
raw: {
id: 'attribute_value_3',
attribute_id: 'attribute_3',
run_id: 'playbook_run_3',
value: 'Invalid Value',
},
},
}),
).rejects.toThrow('Record not found for non create action');
});
it('=> transformPlaybookRunAttributeValueRecord: should keep most of the data if the partial attribute value is empty', async () => {
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
expect(database).toBeTruthy();
let existingRecord: PlaybookRunAttributeValueModel | undefined;
await database!.write(async () => {
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
record._raw.id = 'attribute_value_2';
record.attributeId = 'attribute_2';
record.runId = 'playbook_run_2';
record.value = 'Existing Value';
});
});
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
action: OperationType.UPDATE,
database: database!,
value: {
record: existingRecord,
raw: {
id: 'attribute_value_2',
run_id: 'playbook_run_2',
attribute_id: 'attribute_2',
},
},
});
await database?.write(async () => {
await database?.batch(preparedRecord);
});
expect(preparedRecord).toBeTruthy();
expect(preparedRecord.attributeId).toBe('attribute_2');
expect(preparedRecord.runId).toBe('playbook_run_2');
expect(preparedRecord.value).toBe('Existing Value');
});
});

View file

@ -8,9 +8,11 @@ import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
import type{TransformerArgs} from '@typings/database/database';
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM} = PLAYBOOK_TABLES;
const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
/**
* transformPlaybookRunRecord: Prepares a record of the SERVER database 'PlaybookRun' table for update or create actions.
@ -140,3 +142,71 @@ export const transformPlaybookChecklistItemRecord = ({action, database, value}:
fieldsMapper,
});
};
/**
* transformPlaybookRunAttributeRecord: Prepares a record of the SERVER database 'PlaybookRunAttribute' table for update or create actions.
* @param {TransformerArgs} transformerArgs
* @param {Database} transformerArgs.database
* @param {RecordPair} transformerArgs.value
* @returns {Promise<PlaybookRunAttributeModel>}
*/
export const transformPlaybookRunAttributeRecord = ({action, database, value}: TransformerArgs<PlaybookRunAttributeModel, PartialPlaybookRunAttribute>): Promise<PlaybookRunAttributeModel> => {
const raw = value.raw;
const record = value.record;
const isCreateAction = action === OperationType.CREATE;
if (!isCreateAction && !record) {
return Promise.reject(new Error('Record not found for non create action'));
}
const fieldsMapper = (attribute: PlaybookRunAttributeModel) => {
attribute._raw.id = isCreateAction ? (raw?.id ?? attribute.id) : record!.id;
attribute.groupId = raw.group_id ?? record?.groupId ?? '';
attribute.name = raw.name ?? record?.name ?? '';
attribute.type = raw.type ?? record?.type ?? '';
attribute.targetId = raw.target_id ?? record?.targetId ?? '';
attribute.targetType = raw.target_type ?? record?.targetType ?? '';
attribute.createAt = raw.create_at ?? record?.createAt ?? 0;
attribute.updateAt = raw.update_at ?? record?.updateAt ?? 0;
attribute.deleteAt = raw.delete_at ?? record?.deleteAt ?? 0;
attribute.attrs = raw.attrs ?? record?.attrs ?? '';
};
return prepareBaseRecord({
action,
database,
tableName: PLAYBOOK_RUN_ATTRIBUTE,
value,
fieldsMapper,
});
};
/**
* transformPlaybookRunAttributeValueRecord: Prepares a record of the SERVER database 'PlaybookRunAttributeValue' table for update or create actions.
* @param {TransformerArgs} transformerArgs
* @param {Database} transformerArgs.database
* @param {RecordPair} transformerArgs.value
* @returns {Promise<PlaybookRunAttributeValueModel>}
*/
export const transformPlaybookRunAttributeValueRecord = ({action, database, value}: TransformerArgs<PlaybookRunAttributeValueModel, PartialPlaybookRunAttributeValue>): Promise<PlaybookRunAttributeValueModel> => {
const raw = value.raw;
const record = value.record;
const isCreateAction = action === OperationType.CREATE;
if (!isCreateAction && !record) {
return Promise.reject(new Error('Record not found for non create action'));
}
const fieldsMapper = (attribute: PlaybookRunAttributeValueModel) => {
attribute._raw.id = isCreateAction ? (raw?.id ?? attribute.id) : record!.id;
attribute.attributeId = raw.attribute_id ?? record?.attributeId ?? '';
attribute.runId = raw.run_id ?? record?.runId ?? '';
attribute.value = raw.value ?? record?.value ?? '';
};
return prepareBaseRecord({
action,
database,
tableName: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
value,
fieldsMapper,
});
};

View file

@ -4,3 +4,5 @@
export {default as PlaybookRunSchema} from './playbook_run';
export {default as PlaybookChecklistSchema} from './playbook_checklist';
export {default as PlaybookChecklistItemSchema} from './playbook_checklist_item';
export {default as PlaybookRunAttributeSchema} from './playbook_run_attribute';
export {default as PlaybookRunAttributeValueSchema} from './playbook_run_attribute_value';

View file

@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {tableSchema} from '@nozbe/watermelondb';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
const {PLAYBOOK_RUN_ATTRIBUTE} = PLAYBOOK_TABLES;
/**
* The schema for the PLAYBOOK_RUN_ATTRIBUTE table.
* notice that this mimics closely the Custom Profile Attribute table schema. As there is a base element to both on properties.
* After discussing, we decided to have a separate client schema for each feature based on System Attributes.
* although we moved away from field/attribute to an attribute/attribute value which might have some initial confusion but
* will eventually be more consistent.
*/
export default tableSchema({
name: PLAYBOOK_RUN_ATTRIBUTE,
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},
],
});

View file

@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {tableSchema} from '@nozbe/watermelondb';
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
const {PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES;
/**
* The schema for the PLAYBOOK_RUN_ATTRIBUTE_VALUE table.
* notice that this mimics closely the Custom Profile Attribute table schema. As there is a base element to both on properties.
* After discussing, we decided to have a separate client schema for each feature based on System Attributes.
* although we moved away from field/attribute to an attribute/attribute value which might have some initial confusion but
* will eventually be more consistent.
*/
export default tableSchema({
name: PLAYBOOK_RUN_ATTRIBUTE_VALUE,
columns: [
{name: 'attribute_id', type: 'string', isIndexed: true},
{name: 'run_id', type: 'string', isIndexed: true},
{name: 'value', type: 'string'},
],
});

View file

@ -71,6 +71,26 @@ type TimelineEvent = {
type PlaybookRunStatusType = typeof PlaybookRunStatus[keyof typeof PlaybookRunStatus];
type PlaybookRunAttribute = {
id: string;
group_id: string;
name: string;
type: string;
target_id: string;
target_type: string;
create_at: number;
update_at: number;
delete_at: number;
attrs?: string;
}
type PlaybookRunAttributeValue = {
id: string;
attribute_id: string;
run_id: string;
value: string;
}
type PlaybookRun = {
id: string;
name: string;

View file

@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type Model from '@nozbe/watermelondb/Model';
/**
* The PlaybookRunAttribute interface, which will have all the fields from the PLAYBOOK_RUN_ATTRIBUTE table
*/
interface PlaybookRunAttributeModelInterface extends Model {
/** groupId : The group ID of the field */
groupId: string;
/** name : The name of the field */
name: string;
/** type : The type of the field */
type: string;
/** targetId : The target ID of the field */
targetId: string;
/** targetType : The target type of the field */
targetType: string;
/** createAt : The timestamp of when the field was created */
createAt: number;
/** updateAt : The timestamp of when the field was last updated */
updateAt: number;
/** deleteAt : The timestamp of when the field was deleted */
deleteAt: number;
/** attrs : Additional attributes for the field (JSON string) */
attrs?: string;
}
export type {PlaybookRunAttributeModelInterface as default};

View file

@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type PlaybookRunModel from './playbook_run';
import type PlaybookRunAttributeModel from './playbook_run_attribute';
import type {Relation} from '@nozbe/watermelondb';
import type Model from '@nozbe/watermelondb/Model';
/**
* The PlaybookRunAttributeValue interface, which will have all the fields from the PLAYBOOK_RUN_ATTRIBUTE_VALUE table
*/
interface PlaybookRunAttributeValueModelInterface extends Model {
/** attributeId : The ID of the attribute this attribute value belongs to */
attributeId: string;
/** runId : The ID of the playbook run this attribute belongs to */
runId: string;
/** value : The value of the attribute */
value: string;
/** attribute : The attribute this attribute value belongs to */
attribute: Relation<PlaybookRunAttributeModel>;
/** run : The playbook run this attribute belongs to */
run: Relation<PlaybookRunModel>;
}
export type {PlaybookRunAttributeValueModelInterface as default};

View file

@ -7,8 +7,16 @@ type WithRunId = {
type WithChecklistId = {
checklist_id: string;
}
type WithAttributeId = {
attribute_id: string;
}
type PartialWithId<T extends {id: string}> = Partial<T> & Pick<T, 'id'>;
type PartialPlaybookRun = PartialWithId<PlaybookRun>;
type PartialChecklist = PartialWithId<PlaybookChecklist> & WithRunId;
type PartialChecklistItem = PartialWithId<PlaybookChecklistItem> & WithChecklistId;
type PartialPlaybookRunAttribute = PartialWithId<PlaybookRunAttribute>;
type PartialPlaybookRunAttributeValue = PartialWithId<PlaybookRunAttributeValue> & WithRunId & WithAttributeId;

View file

@ -1,4 +1,4 @@
# Server Database - Schema Version 13
# Server Database - Schema Version 14
# 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.
@ -230,6 +230,26 @@ completed_at number
synced string NULL INDEX # optional field for sync status
last_sync_at number NULL # optional field for last sync timestamp
PlaybookRunAttribute
-
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
PlaybookRunAttributeValue
-
id PK string # composition ID PlaybookRun.id-PlaybookRunAttribute.id
attribute_id string INDEX FK >- PlaybookRunAttribute.id
run_id string INDEX FK >- PlaybookRun.id
value string
Post
-
id PK string # server generated

View file

@ -23,6 +23,8 @@ import type {Model, Query, Relation} from '@nozbe/watermelondb';
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
import type PlaybookRunAttributeModel from '@playbooks/types/database/models/playbook_run_attribute';
import type PlaybookRunAttributeValueModel from '@playbooks/types/database/models/playbook_run_attribute_value';
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
import type ChannelModel from '@typings/database/models/servers/channel';
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
@ -1067,7 +1069,7 @@ class TestHelperSingleton {
};
};
createPlaybookItem =(prefix: string, index: number): PlaybookChecklistItem => ({
createPlaybookItem = (prefix: string, index: number): PlaybookChecklistItem => ({
id: `${prefix}-item_${index}`,
title: `Item ${index + 1} of Checklist ${prefix}`,
description: 'Item description',
@ -1085,6 +1087,26 @@ class TestHelperSingleton {
update_at: 0,
});
createPlaybookRunAttribute = (prefix: string, index: number): PlaybookRunAttribute => ({
id: `${prefix}-attribute_${index}`,
group_id: 'group_1',
name: `Attribute ${index + 1}`,
type: 'text',
target_id: `${prefix}`,
target_type: 'playbook_run',
create_at: Date.now(),
update_at: Date.now(),
delete_at: 0,
attrs: '',
});
createPlaybookRunAttributeValue = (attributeId: string, runId: string, index: number): PlaybookRunAttributeValue => ({
id: `${runId}-${attributeId}-value_${index}`,
attribute_id: attributeId,
run_id: runId,
value: `Value ${index + 1}`,
});
createPlaybookChecklist = (prefix: string, itemsCount: number, index: number): PlaybookChecklist => {
const items: PlaybookChecklistItem[] = [];
const id = `${prefix}-checklist_${index}`;
@ -1263,6 +1285,60 @@ class TestHelperSingleton {
};
};
fakePlaybookRunAttribute = (overwrite: Partial<PlaybookRunAttribute> = {}): PlaybookRunAttribute => {
return {
id: this.generateId(),
group_id: this.generateId(),
name: 'Test Attribute',
type: 'text',
target_id: this.generateId(),
target_type: 'playbook_run',
create_at: Date.now(),
update_at: Date.now(),
delete_at: 0,
attrs: '',
...overwrite,
};
};
fakePlaybookRunAttributeValue = (attributeId: string, runId: string, overwrite: Partial<PlaybookRunAttributeValue> = {}): PlaybookRunAttributeValue => {
return {
id: this.generateId(),
attribute_id: attributeId,
run_id: runId,
value: 'Test Value',
...overwrite,
};
};
fakePlaybookRunAttributeModel = (overwrite: Partial<PlaybookRunAttributeModel> = {}): PlaybookRunAttributeModel => {
return {
...this.fakeModel(),
groupId: this.generateId(),
name: 'Test Attribute',
type: 'text',
targetId: this.generateId(),
targetType: 'playbook_run',
createAt: Date.now(),
updateAt: Date.now(),
deleteAt: 0,
attrs: '',
...overwrite,
};
};
fakePlaybookRunAttributeValueModel = (overwrite: Partial<PlaybookRunAttributeValueModel> = {}): PlaybookRunAttributeValueModel => {
return {
...this.fakeModel(),
attributeId: this.generateId(),
runId: this.generateId(),
value: 'Test Value',
attribute: this.fakeRelation(),
run: this.fakeRelation(),
...overwrite,
};
};
fakeWebsocketMessage = (overwrite: Partial<WebSocketMessage> = {}): WebSocketMessage => {
return {
event: 'test',