Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update topic schema + add migration for weighted topics #9153

Merged
merged 11 commits into from
Sep 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function CreateContestManagerMetadata(): Command<
}
contestTopics = topics.map((t) => t.get({ plain: true }));
contestTopicsToCreate = topics.map((t) => ({
weighted_voting: schemas.TopicWeightedVoting.Stake,
contest_address: rest.contest_address,
topic_id: t.id!,
created_at: new Date(),
Expand Down
14 changes: 11 additions & 3 deletions libs/model/src/models/contest_topic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ContestTopic } from '@hicommonwealth/schemas';
import { ContestTopic as ContestTopicSchema } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import { z } from 'zod';
import type { ModelInstance } from './types';

type ContestTopic = ModelInstance<z.infer<typeof ContestTopic>>;
type ContestTopic = ModelInstance<z.infer<typeof ContestTopicSchema>>;

export default (
sequelize: Sequelize.Sequelize,
Expand All @@ -13,6 +13,8 @@ export default (
{
contest_address: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
primaryKey: true,
},
topic_id: { type: Sequelize.INTEGER, primaryKey: true },
Expand All @@ -21,6 +23,12 @@ export default (
{
tableName: 'ContestTopics',
timestamps: false,
indexes: [],
indexes: [
{
unique: true,
fields: ['contest_address'],
name: 'contest_address_unique',
},
],
},
);
20 changes: 20 additions & 0 deletions libs/model/src/models/topic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TopicWeightedVoting } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import type { CommunityAttributes } from './community';
import type { ThreadAttributes } from './thread';
Expand All @@ -19,6 +20,12 @@ export type TopicAttributes = {
group_ids?: number[];
telegram?: string;

weighted_voting?: TopicWeightedVoting;
chain_node_id?: number;
token_address?: string;
token_symbol?: string;
vote_weight_multiplier?: number;

// associations
community?: CommunityAttributes;
threads?: ThreadAttributes[] | TopicAttributes['id'][];
Expand Down Expand Up @@ -63,6 +70,19 @@ export default (
defaultValue: [],
},
telegram: { type: Sequelize.STRING, allowNull: true },
weighted_voting: { type: Sequelize.STRING, allowNull: true },
chain_node_id: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'ChainNodes',
key: 'id',
},
onUpdate: 'CASCADE',
},
token_address: { type: Sequelize.STRING, allowNull: true },
token_symbol: { type: Sequelize.STRING, allowNull: true },
vote_weight_multiplier: { type: Sequelize.INTEGER, allowNull: true },
},
{
timestamps: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('Contests metadata commands lifecycle', () => {
interval,
ticker,
decimals,
topic_ids: topics.map((t) => t.id!),
topic_ids: [topics[0].id!],
},
},
);
Expand All @@ -213,14 +213,6 @@ describe('Contests metadata commands lifecycle', () => {
id: topics[0].id,
name: topics[0].name,
});
expect(createResult!.contest_managers![0].topics![1]).to.deep.contain({
id: topics[1].id,
name: topics[1].name,
});
expect(createResult!.contest_managers![0].topics![2]).to.deep.contain({
id: topics[2].id,
name: topics[2].name,
});
});
});

Expand Down Expand Up @@ -314,15 +306,14 @@ describe('Contests metadata commands lifecycle', () => {
payload: {
id: community_id,
contest_address,
topic_ids: [topics[0]!.id!, topics[1]!.id!],
topic_ids: [topics[0]!.id!],
},
},
);
const metadata = updateResult?.contest_managers![0];
expect(metadata!.topics).to.have.length(2);
expect(metadata!.topics).to.have.length(1);
const resultTopicIds = metadata!.topics!.map((t) => t.id);
expect(resultTopicIds).to.contain(topics[0]!.id!);
expect(resultTopicIds).to.contain(topics[1]!.id!);
}

{
Expand Down
4 changes: 2 additions & 2 deletions libs/schemas/src/commands/contest.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const CreateContestManagerMetadata = {
decimals: PG_INT.optional().default(
commonProtocol.WeiDecimals[commonProtocol.Denominations.ETH],
),
topic_ids: z.array(z.number()).optional(),
topic_ids: z.array(z.number()).max(1).optional(),
}),
output: z.object({
contest_managers: z.array(ContestManager),
Expand All @@ -43,7 +43,7 @@ export const UpdateContestManagerMetadata = {
contest_address: z.string().describe('On-Chain contest manager address'),
name: z.string().optional(),
image_url: z.string().optional(),
topic_ids: z.array(z.number()).optional(),
topic_ids: z.array(z.number()).max(1).optional(),
}),
output: z.object({
contest_managers: z.array(ContestManager),
Expand Down
20 changes: 20 additions & 0 deletions libs/schemas/src/entities/topic.schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { z } from 'zod';
import { PG_INT } from '../utils';

export enum TopicWeightedVoting {
Stake = 'stake',
ERC20 = 'erc20',
}

export const Topic = z.object({
id: PG_INT.optional(),
name: z.string().max(255).default('General'),
Expand All @@ -14,4 +19,19 @@ export const Topic = z.object({
channel_id: z.string().max(255).nullish(),
group_ids: z.array(PG_INT).default([]),
default_offchain_template_backup: z.string().nullish(),
weighted_voting: z.nativeEnum(TopicWeightedVoting).nullish(),
chain_node_id: PG_INT.nullish().describe(
'token chain node ID, used for ERC20 topics',
),
token_address: z
.string()
.nullish()
.describe('token address, used for ERC20 topics'),
token_symbol: z
.string()
.nullish()
.describe('token symbol, used for ERC20 topics'),
vote_weight_multiplier: PG_INT.nullish().describe(
'vote weight multiplier, used for ERC20 topics',
),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.addColumn(
'Topics',
'weighted_voting',
{
type: Sequelize.STRING,
allowNull: true,
},
{ transaction },
);

await queryInterface.addColumn(
'Topics',
'chain_node_id',
{
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'ChainNodes',
key: 'id',
},
onUpdate: 'CASCADE',
},
{ transaction },
);

await queryInterface.addColumn(
'Topics',
'token_address',
{
type: Sequelize.STRING,
allowNull: true,
},
{ transaction },
);

await queryInterface.addColumn(
'Topics',
'token_symbol',
{
type: Sequelize.STRING,
allowNull: true,
},
{ transaction },
);

await queryInterface.addColumn(
'Topics',
'vote_weight_multiplier',
{
type: Sequelize.INTEGER,
allowNull: true,
},
{ transaction },
);

// delete duplicate ContestTopic entries to ensure that
// all contests only associate with a single topic
await queryInterface.sequelize.query(
`
DELETE FROM "ContestTopics"
WHERE (contest_address, topic_id) NOT IN (
SELECT contest_address, topic_id
FROM "ContestTopics" AS ct1
WHERE topic_id = (
SELECT MIN(topic_id)
FROM "ContestTopics" AS ct2
WHERE ct1.contest_address = ct2.contest_address
)
);
`,
{ transaction },
);

// ensure no duplicate ContestTopics by contest_address from now on
await queryInterface.addConstraint('ContestTopics', {
fields: ['contest_address'],
type: 'unique',
name: 'contest_topics_address_unique',
transaction,
});
});
},

down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.sequelize.query(
`
ALTER TABLE "ContestTopics"
DROP CONSTRAINT contest_address_unique;
`,
{ transaction },
);

// cannot restore deleted ContestTopics

await queryInterface.removeColumn('Topics', 'weighted_voting', {
transaction,
});
await queryInterface.removeColumn('Topics', 'chain_node_id', {
transaction,
});
await queryInterface.removeColumn('Topics', 'token_address', {
transaction,
});
await queryInterface.removeColumn('Topics', 'token_symbol', {
transaction,
});
await queryInterface.removeColumn('Topics', 'vote_weight_multiplier', {
transaction,
});
});
},
};
Loading