Browse Source

Forum - threads and categories mappings & tests - part 1

Leszek Wiesner 3 years ago
parent
commit
a6457314f2
37 changed files with 3174 additions and 597 deletions
  1. 2 2
      .github/workflows/run-integration-tests.yml
  2. 191 2
      metadata-protobuf/compiled/index.d.ts
  3. 391 7
      metadata-protobuf/compiled/index.js
  4. 146 12
      metadata-protobuf/doc/index.md
  5. 1 1
      metadata-protobuf/package.json
  6. 14 0
      metadata-protobuf/proto/Forum.proto
  7. 2 1
      package.json
  8. 1 0
      query-node/build.sh
  9. 12 13
      query-node/manifest.yml
  10. 6 1
      query-node/mappings/common.ts
  11. 336 15
      query-node/mappings/forum.ts
  12. 1 0
      query-node/mappings/init.ts
  13. 22 16
      query-node/schemas/forum.graphql
  14. 15 10
      query-node/schemas/forumEvents.graphql
  15. 3 0
      query-node/schemas/workingGroups.graphql
  16. 38 1
      tests/integration-tests/src/Api.ts
  17. 103 3
      tests/integration-tests/src/QueryNodeApi.ts
  18. 2 0
      tests/integration-tests/src/consts.ts
  19. 82 0
      tests/integration-tests/src/fixtures/forum/CreateCategoriesFixture.ts
  20. 139 0
      tests/integration-tests/src/fixtures/forum/CreateThreadsFixture.ts
  21. 96 0
      tests/integration-tests/src/fixtures/forum/DeleteThreadsFixture.ts
  22. 79 0
      tests/integration-tests/src/fixtures/forum/RemoveCategoriesFixture.ts
  23. 89 0
      tests/integration-tests/src/fixtures/forum/UpdateCategoriesStatusFixture.ts
  24. 78 0
      tests/integration-tests/src/fixtures/forum/VoteOnPollFixture.ts
  25. 19 0
      tests/integration-tests/src/fixtures/forum/WithForumLeadFixture.ts
  26. 6 0
      tests/integration-tests/src/fixtures/forum/index.ts
  27. 86 0
      tests/integration-tests/src/flows/forum/categories.ts
  28. 72 0
      tests/integration-tests/src/flows/forum/polls.ts
  29. 59 0
      tests/integration-tests/src/flows/forum/threads.ts
  30. 386 16
      tests/integration-tests/src/graphql/generated/queries.ts
  31. 358 391
      tests/integration-tests/src/graphql/generated/schema.ts
  32. 98 0
      tests/integration-tests/src/graphql/queries/forum.graphql
  33. 112 0
      tests/integration-tests/src/graphql/queries/forumEvents.graphql
  34. 12 0
      tests/integration-tests/src/scenarios/forum.ts
  35. 40 2
      tests/integration-tests/src/scenarios/full.ts
  36. 44 7
      tests/integration-tests/src/types.ts
  37. 33 97
      yarn.lock

+ 2 - 2
.github/workflows/run-integration-tests.yml

@@ -25,7 +25,7 @@ jobs:
       - uses: actions/checkout@v1
       - uses: actions/setup-node@v1
         with:
-          node-version: '12.x'
+          node-version: '14.x'
 
       - id: compute_shasum
         name: Compute runtime code shasum
@@ -86,7 +86,7 @@ jobs:
       - uses: actions/checkout@v1
       - uses: actions/setup-node@v1
         with:
-          node-version: '12.x'
+          node-version: '14.x'
       - name: Get artifacts
         uses: actions/download-artifact@v2
         with:

+ 191 - 2
metadata-protobuf/compiled/index.d.ts

@@ -107,6 +107,195 @@ export class CouncilCandidacyNoteMetadata implements ICouncilCandidacyNoteMetada
     public toJSON(): { [k: string]: any };
 }
 
+/** Properties of a ForumPostReaction. */
+export interface IForumPostReaction {
+}
+
+/** Represents a ForumPostReaction. */
+export class ForumPostReaction implements IForumPostReaction {
+
+    /**
+     * Constructs a new ForumPostReaction.
+     * @param [properties] Properties to set
+     */
+    constructor(properties?: IForumPostReaction);
+
+    /**
+     * Creates a new ForumPostReaction instance using the specified properties.
+     * @param [properties] Properties to set
+     * @returns ForumPostReaction instance
+     */
+    public static create(properties?: IForumPostReaction): ForumPostReaction;
+
+    /**
+     * Encodes the specified ForumPostReaction message. Does not implicitly {@link ForumPostReaction.verify|verify} messages.
+     * @param message ForumPostReaction message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encode(message: IForumPostReaction, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Encodes the specified ForumPostReaction message, length delimited. Does not implicitly {@link ForumPostReaction.verify|verify} messages.
+     * @param message ForumPostReaction message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encodeDelimited(message: IForumPostReaction, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Decodes a ForumPostReaction message from the specified reader or buffer.
+     * @param reader Reader or buffer to decode from
+     * @param [length] Message length if known beforehand
+     * @returns ForumPostReaction
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ForumPostReaction;
+
+    /**
+     * Decodes a ForumPostReaction message from the specified reader or buffer, length delimited.
+     * @param reader Reader or buffer to decode from
+     * @returns ForumPostReaction
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ForumPostReaction;
+
+    /**
+     * Verifies a ForumPostReaction message.
+     * @param message Plain object to verify
+     * @returns `null` if valid, otherwise the reason why it is not
+     */
+    public static verify(message: { [k: string]: any }): (string|null);
+
+    /**
+     * Creates a ForumPostReaction message from a plain object. Also converts values to their respective internal types.
+     * @param object Plain object
+     * @returns ForumPostReaction
+     */
+    public static fromObject(object: { [k: string]: any }): ForumPostReaction;
+
+    /**
+     * Creates a plain object from a ForumPostReaction message. Also converts values to other types if specified.
+     * @param message ForumPostReaction
+     * @param [options] Conversion options
+     * @returns Plain object
+     */
+    public static toObject(message: ForumPostReaction, options?: $protobuf.IConversionOptions): { [k: string]: any };
+
+    /**
+     * Converts this ForumPostReaction to JSON.
+     * @returns JSON object
+     */
+    public toJSON(): { [k: string]: any };
+}
+
+export namespace ForumPostReaction {
+
+    /** Reaction enum. */
+    enum Reaction {
+        CANCEL = 0,
+        LIKE = 1
+    }
+}
+
+/** Properties of a ForumPostMetadata. */
+export interface IForumPostMetadata {
+
+    /** ForumPostMetadata text */
+    text?: (string|null);
+
+    /** ForumPostMetadata repliesTo */
+    repliesTo?: (number|null);
+}
+
+/** Represents a ForumPostMetadata. */
+export class ForumPostMetadata implements IForumPostMetadata {
+
+    /**
+     * Constructs a new ForumPostMetadata.
+     * @param [properties] Properties to set
+     */
+    constructor(properties?: IForumPostMetadata);
+
+    /** ForumPostMetadata text. */
+    public text: string;
+
+    /** ForumPostMetadata repliesTo. */
+    public repliesTo: number;
+
+    /**
+     * Creates a new ForumPostMetadata instance using the specified properties.
+     * @param [properties] Properties to set
+     * @returns ForumPostMetadata instance
+     */
+    public static create(properties?: IForumPostMetadata): ForumPostMetadata;
+
+    /**
+     * Encodes the specified ForumPostMetadata message. Does not implicitly {@link ForumPostMetadata.verify|verify} messages.
+     * @param message ForumPostMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encode(message: IForumPostMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Encodes the specified ForumPostMetadata message, length delimited. Does not implicitly {@link ForumPostMetadata.verify|verify} messages.
+     * @param message ForumPostMetadata message or plain object to encode
+     * @param [writer] Writer to encode to
+     * @returns Writer
+     */
+    public static encodeDelimited(message: IForumPostMetadata, writer?: $protobuf.Writer): $protobuf.Writer;
+
+    /**
+     * Decodes a ForumPostMetadata message from the specified reader or buffer.
+     * @param reader Reader or buffer to decode from
+     * @param [length] Message length if known beforehand
+     * @returns ForumPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): ForumPostMetadata;
+
+    /**
+     * Decodes a ForumPostMetadata message from the specified reader or buffer, length delimited.
+     * @param reader Reader or buffer to decode from
+     * @returns ForumPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): ForumPostMetadata;
+
+    /**
+     * Verifies a ForumPostMetadata message.
+     * @param message Plain object to verify
+     * @returns `null` if valid, otherwise the reason why it is not
+     */
+    public static verify(message: { [k: string]: any }): (string|null);
+
+    /**
+     * Creates a ForumPostMetadata message from a plain object. Also converts values to their respective internal types.
+     * @param object Plain object
+     * @returns ForumPostMetadata
+     */
+    public static fromObject(object: { [k: string]: any }): ForumPostMetadata;
+
+    /**
+     * Creates a plain object from a ForumPostMetadata message. Also converts values to other types if specified.
+     * @param message ForumPostMetadata
+     * @param [options] Conversion options
+     * @returns Plain object
+     */
+    public static toObject(message: ForumPostMetadata, options?: $protobuf.IConversionOptions): { [k: string]: any };
+
+    /**
+     * Converts this ForumPostMetadata to JSON.
+     * @returns JSON object
+     */
+    public toJSON(): { [k: string]: any };
+}
+
 /** Properties of a MembershipMetadata. */
 export interface IMembershipMetadata {
 
@@ -431,8 +620,8 @@ export namespace OpeningMetadata {
 
         /** InputType enum. */
         enum InputType {
-            TEXT = 0,
-            TEXTAREA = 1
+            TEXTAREA = 0,
+            TEXT = 1
         }
     }
 }

+ 391 - 7
metadata-protobuf/compiled/index.js

@@ -280,6 +280,390 @@ $root.CouncilCandidacyNoteMetadata = (function() {
     return CouncilCandidacyNoteMetadata;
 })();
 
+$root.ForumPostReaction = (function() {
+
+    /**
+     * Properties of a ForumPostReaction.
+     * @exports IForumPostReaction
+     * @interface IForumPostReaction
+     */
+
+    /**
+     * Constructs a new ForumPostReaction.
+     * @exports ForumPostReaction
+     * @classdesc Represents a ForumPostReaction.
+     * @implements IForumPostReaction
+     * @constructor
+     * @param {IForumPostReaction=} [properties] Properties to set
+     */
+    function ForumPostReaction(properties) {
+        if (properties)
+            for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * Creates a new ForumPostReaction instance using the specified properties.
+     * @function create
+     * @memberof ForumPostReaction
+     * @static
+     * @param {IForumPostReaction=} [properties] Properties to set
+     * @returns {ForumPostReaction} ForumPostReaction instance
+     */
+    ForumPostReaction.create = function create(properties) {
+        return new ForumPostReaction(properties);
+    };
+
+    /**
+     * Encodes the specified ForumPostReaction message. Does not implicitly {@link ForumPostReaction.verify|verify} messages.
+     * @function encode
+     * @memberof ForumPostReaction
+     * @static
+     * @param {IForumPostReaction} message ForumPostReaction message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumPostReaction.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        return writer;
+    };
+
+    /**
+     * Encodes the specified ForumPostReaction message, length delimited. Does not implicitly {@link ForumPostReaction.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof ForumPostReaction
+     * @static
+     * @param {IForumPostReaction} message ForumPostReaction message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumPostReaction.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes a ForumPostReaction message from the specified reader or buffer.
+     * @function decode
+     * @memberof ForumPostReaction
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {ForumPostReaction} ForumPostReaction
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumPostReaction.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ForumPostReaction();
+        while (reader.pos < end) {
+            var tag = reader.uint32();
+            switch (tag >>> 3) {
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes a ForumPostReaction message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof ForumPostReaction
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {ForumPostReaction} ForumPostReaction
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumPostReaction.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies a ForumPostReaction message.
+     * @function verify
+     * @memberof ForumPostReaction
+     * @static
+     * @param {Object.<string,*>} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    ForumPostReaction.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        return null;
+    };
+
+    /**
+     * Creates a ForumPostReaction message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof ForumPostReaction
+     * @static
+     * @param {Object.<string,*>} object Plain object
+     * @returns {ForumPostReaction} ForumPostReaction
+     */
+    ForumPostReaction.fromObject = function fromObject(object) {
+        if (object instanceof $root.ForumPostReaction)
+            return object;
+        return new $root.ForumPostReaction();
+    };
+
+    /**
+     * Creates a plain object from a ForumPostReaction message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof ForumPostReaction
+     * @static
+     * @param {ForumPostReaction} message ForumPostReaction
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.<string,*>} Plain object
+     */
+    ForumPostReaction.toObject = function toObject() {
+        return {};
+    };
+
+    /**
+     * Converts this ForumPostReaction to JSON.
+     * @function toJSON
+     * @memberof ForumPostReaction
+     * @instance
+     * @returns {Object.<string,*>} JSON object
+     */
+    ForumPostReaction.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    /**
+     * Reaction enum.
+     * @name ForumPostReaction.Reaction
+     * @enum {number}
+     * @property {number} CANCEL=0 CANCEL value
+     * @property {number} LIKE=1 LIKE value
+     */
+    ForumPostReaction.Reaction = (function() {
+        var valuesById = {}, values = Object.create(valuesById);
+        values[valuesById[0] = "CANCEL"] = 0;
+        values[valuesById[1] = "LIKE"] = 1;
+        return values;
+    })();
+
+    return ForumPostReaction;
+})();
+
+$root.ForumPostMetadata = (function() {
+
+    /**
+     * Properties of a ForumPostMetadata.
+     * @exports IForumPostMetadata
+     * @interface IForumPostMetadata
+     * @property {string|null} [text] ForumPostMetadata text
+     * @property {number|null} [repliesTo] ForumPostMetadata repliesTo
+     */
+
+    /**
+     * Constructs a new ForumPostMetadata.
+     * @exports ForumPostMetadata
+     * @classdesc Represents a ForumPostMetadata.
+     * @implements IForumPostMetadata
+     * @constructor
+     * @param {IForumPostMetadata=} [properties] Properties to set
+     */
+    function ForumPostMetadata(properties) {
+        if (properties)
+            for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i)
+                if (properties[keys[i]] != null)
+                    this[keys[i]] = properties[keys[i]];
+    }
+
+    /**
+     * ForumPostMetadata text.
+     * @member {string} text
+     * @memberof ForumPostMetadata
+     * @instance
+     */
+    ForumPostMetadata.prototype.text = "";
+
+    /**
+     * ForumPostMetadata repliesTo.
+     * @member {number} repliesTo
+     * @memberof ForumPostMetadata
+     * @instance
+     */
+    ForumPostMetadata.prototype.repliesTo = 0;
+
+    /**
+     * Creates a new ForumPostMetadata instance using the specified properties.
+     * @function create
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {IForumPostMetadata=} [properties] Properties to set
+     * @returns {ForumPostMetadata} ForumPostMetadata instance
+     */
+    ForumPostMetadata.create = function create(properties) {
+        return new ForumPostMetadata(properties);
+    };
+
+    /**
+     * Encodes the specified ForumPostMetadata message. Does not implicitly {@link ForumPostMetadata.verify|verify} messages.
+     * @function encode
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {IForumPostMetadata} message ForumPostMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumPostMetadata.encode = function encode(message, writer) {
+        if (!writer)
+            writer = $Writer.create();
+        if (message.text != null && Object.hasOwnProperty.call(message, "text"))
+            writer.uint32(/* id 1, wireType 2 =*/10).string(message.text);
+        if (message.repliesTo != null && Object.hasOwnProperty.call(message, "repliesTo"))
+            writer.uint32(/* id 2, wireType 0 =*/16).uint32(message.repliesTo);
+        return writer;
+    };
+
+    /**
+     * Encodes the specified ForumPostMetadata message, length delimited. Does not implicitly {@link ForumPostMetadata.verify|verify} messages.
+     * @function encodeDelimited
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {IForumPostMetadata} message ForumPostMetadata message or plain object to encode
+     * @param {$protobuf.Writer} [writer] Writer to encode to
+     * @returns {$protobuf.Writer} Writer
+     */
+    ForumPostMetadata.encodeDelimited = function encodeDelimited(message, writer) {
+        return this.encode(message, writer).ldelim();
+    };
+
+    /**
+     * Decodes a ForumPostMetadata message from the specified reader or buffer.
+     * @function decode
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @param {number} [length] Message length if known beforehand
+     * @returns {ForumPostMetadata} ForumPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumPostMetadata.decode = function decode(reader, length) {
+        if (!(reader instanceof $Reader))
+            reader = $Reader.create(reader);
+        var end = length === undefined ? reader.len : reader.pos + length, message = new $root.ForumPostMetadata();
+        while (reader.pos < end) {
+            var tag = reader.uint32();
+            switch (tag >>> 3) {
+            case 1:
+                message.text = reader.string();
+                break;
+            case 2:
+                message.repliesTo = reader.uint32();
+                break;
+            default:
+                reader.skipType(tag & 7);
+                break;
+            }
+        }
+        return message;
+    };
+
+    /**
+     * Decodes a ForumPostMetadata message from the specified reader or buffer, length delimited.
+     * @function decodeDelimited
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from
+     * @returns {ForumPostMetadata} ForumPostMetadata
+     * @throws {Error} If the payload is not a reader or valid buffer
+     * @throws {$protobuf.util.ProtocolError} If required fields are missing
+     */
+    ForumPostMetadata.decodeDelimited = function decodeDelimited(reader) {
+        if (!(reader instanceof $Reader))
+            reader = new $Reader(reader);
+        return this.decode(reader, reader.uint32());
+    };
+
+    /**
+     * Verifies a ForumPostMetadata message.
+     * @function verify
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {Object.<string,*>} message Plain object to verify
+     * @returns {string|null} `null` if valid, otherwise the reason why it is not
+     */
+    ForumPostMetadata.verify = function verify(message) {
+        if (typeof message !== "object" || message === null)
+            return "object expected";
+        if (message.text != null && message.hasOwnProperty("text"))
+            if (!$util.isString(message.text))
+                return "text: string expected";
+        if (message.repliesTo != null && message.hasOwnProperty("repliesTo"))
+            if (!$util.isInteger(message.repliesTo))
+                return "repliesTo: integer expected";
+        return null;
+    };
+
+    /**
+     * Creates a ForumPostMetadata message from a plain object. Also converts values to their respective internal types.
+     * @function fromObject
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {Object.<string,*>} object Plain object
+     * @returns {ForumPostMetadata} ForumPostMetadata
+     */
+    ForumPostMetadata.fromObject = function fromObject(object) {
+        if (object instanceof $root.ForumPostMetadata)
+            return object;
+        var message = new $root.ForumPostMetadata();
+        if (object.text != null)
+            message.text = String(object.text);
+        if (object.repliesTo != null)
+            message.repliesTo = object.repliesTo >>> 0;
+        return message;
+    };
+
+    /**
+     * Creates a plain object from a ForumPostMetadata message. Also converts values to other types if specified.
+     * @function toObject
+     * @memberof ForumPostMetadata
+     * @static
+     * @param {ForumPostMetadata} message ForumPostMetadata
+     * @param {$protobuf.IConversionOptions} [options] Conversion options
+     * @returns {Object.<string,*>} Plain object
+     */
+    ForumPostMetadata.toObject = function toObject(message, options) {
+        if (!options)
+            options = {};
+        var object = {};
+        if (options.defaults) {
+            object.text = "";
+            object.repliesTo = 0;
+        }
+        if (message.text != null && message.hasOwnProperty("text"))
+            object.text = message.text;
+        if (message.repliesTo != null && message.hasOwnProperty("repliesTo"))
+            object.repliesTo = message.repliesTo;
+        return object;
+    };
+
+    /**
+     * Converts this ForumPostMetadata to JSON.
+     * @function toJSON
+     * @memberof ForumPostMetadata
+     * @instance
+     * @returns {Object.<string,*>} JSON object
+     */
+    ForumPostMetadata.prototype.toJSON = function toJSON() {
+        return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
+    };
+
+    return ForumPostMetadata;
+})();
+
 $root.MembershipMetadata = (function() {
 
     /**
@@ -1002,11 +1386,11 @@ $root.OpeningMetadata = (function() {
             if (object.question != null)
                 message.question = String(object.question);
             switch (object.type) {
-            case "TEXT":
+            case "TEXTAREA":
             case 0:
                 message.type = 0;
                 break;
-            case "TEXTAREA":
+            case "TEXT":
             case 1:
                 message.type = 1;
                 break;
@@ -1029,7 +1413,7 @@ $root.OpeningMetadata = (function() {
             var object = {};
             if (options.defaults) {
                 object.question = "";
-                object.type = options.enums === String ? "TEXT" : 0;
+                object.type = options.enums === String ? "TEXTAREA" : 0;
             }
             if (message.question != null && message.hasOwnProperty("question"))
                 object.question = message.question;
@@ -1053,13 +1437,13 @@ $root.OpeningMetadata = (function() {
          * InputType enum.
          * @name OpeningMetadata.ApplicationFormQuestion.InputType
          * @enum {number}
-         * @property {number} TEXT=0 TEXT value
-         * @property {number} TEXTAREA=1 TEXTAREA value
+         * @property {number} TEXTAREA=0 TEXTAREA value
+         * @property {number} TEXT=1 TEXT value
          */
         ApplicationFormQuestion.InputType = (function() {
             var valuesById = {}, values = Object.create(valuesById);
-            values[valuesById[0] = "TEXT"] = 0;
-            values[valuesById[1] = "TEXTAREA"] = 1;
+            values[valuesById[0] = "TEXTAREA"] = 0;
+            values[valuesById[1] = "TEXT"] = 1;
             return values;
         })();
 

+ 146 - 12
metadata-protobuf/doc/index.md

@@ -6,14 +6,24 @@
 - [proto/Council.proto](#proto/Council.proto)
     - [CouncilCandidacyNoteMetadata](#.CouncilCandidacyNoteMetadata)
   
+- [proto/Forum.proto](#proto/Forum.proto)
+    - [ForumPostMetadata](#.ForumPostMetadata)
+  
+    - [ForumPostReaction](#.ForumPostReaction)
+  
 - [proto/Membership.proto](#proto/Membership.proto)
     - [MembershipMetadata](#.MembershipMetadata)
   
 - [proto/WorkingGroups.proto](#proto/WorkingGroups.proto)
+    - [AddUpcomingOpening](#.AddUpcomingOpening)
     - [ApplicationMetadata](#.ApplicationMetadata)
     - [OpeningMetadata](#.OpeningMetadata)
     - [OpeningMetadata.ApplicationFormQuestion](#.OpeningMetadata.ApplicationFormQuestion)
-    - [WorkingGroupStatusMetadata](#.WorkingGroupStatusMetadata)
+    - [RemoveUpcomingOpening](#.RemoveUpcomingOpening)
+    - [SetGroupMetadata](#.SetGroupMetadata)
+    - [UpcomingOpeningMetadata](#.UpcomingOpeningMetadata)
+    - [WorkingGroupMetadata](#.WorkingGroupMetadata)
+    - [WorkingGroupMetadataAction](#.WorkingGroupMetadataAction)
   
     - [OpeningMetadata.ApplicationFormQuestion.InputType](#.OpeningMetadata.ApplicationFormQuestion.InputType)
   
@@ -55,6 +65,50 @@
 
 
 
+<a name="proto/Forum.proto"></a>
+<p align="right"><a href="#top">Top</a></p>
+
+## proto/Forum.proto
+
+
+
+<a name=".ForumPostMetadata"></a>
+
+### ForumPostMetadata
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| text | [string](#string) | optional | Post text content (md-formatted) |
+| repliesTo | [uint32](#uint32) | optional | Id of the post that given post replies to (if any) |
+
+
+
+
+
+ 
+
+
+<a name=".ForumPostReaction"></a>
+
+### ForumPostReaction
+
+
+| Name | Number | Description |
+| ---- | ------ | ----------- |
+| CANCEL | 0 | This means cancelling any previous reaction |
+| LIKE | 1 |  |
+
+
+ 
+
+ 
+
+ 
+
+
+
 <a name="proto/Membership.proto"></a>
 <p align="right"><a href="#top">Top</a></p>
 
@@ -71,7 +125,7 @@
 | Field | Type | Label | Description |
 | ----- | ---- | ----- | ----------- |
 | name | [string](#string) | optional | Member&#39;s real name |
-| avatar_uri | [string](#string) | optional | Member&#39;s avatar image uri |
+| avatar | [uint32](#uint32) | optional | Member&#39;s avatar - index into external [assets array](#.Assets) |
 | about | [string](#string) | optional | Member&#39;s md-formatted about text |
 
 
@@ -95,6 +149,21 @@
 
 
 
+<a name=".AddUpcomingOpening"></a>
+
+### AddUpcomingOpening
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| metadata | [UpcomingOpeningMetadata](#UpcomingOpeningMetadata) | optional | Upcoming opening metadata |
+
+
+
+
+
+
 <a name=".ApplicationMetadata"></a>
 
 ### ApplicationMetadata
@@ -118,11 +187,11 @@
 
 | Field | Type | Label | Description |
 | ----- | ---- | ----- | ----------- |
-| short_description | [string](#string) | required | Short description of the opening |
-| description | [string](#string) | required | Full description of the opening |
-| hiring_limit | [uint32](#uint32) | required | Expected number of hired applicants |
-| expected_ending_timestamp | [uint64](#uint64) | required | Expected time when the opening will close (Unix timestamp) |
-| application_details | [string](#string) | required | Md-formatted text explaining the application process |
+| short_description | [string](#string) | optional | Short description of the opening |
+| description | [string](#string) | optional | Full description of the opening |
+| hiring_limit | [uint32](#uint32) | optional | Expected number of hired applicants |
+| expected_ending_timestamp | [uint32](#uint32) | optional | Expected time when the opening will close (Unix timestamp) |
+| application_details | [string](#string) | optional | Md-formatted text explaining the application process |
 | application_form_questions | [OpeningMetadata.ApplicationFormQuestion](#OpeningMetadata.ApplicationFormQuestion) | repeated | List of questions that should be answered during application |
 
 
@@ -138,17 +207,65 @@
 
 | Field | Type | Label | Description |
 | ----- | ---- | ----- | ----------- |
-| question | [string](#string) | required | The question itself (ie. &#34;What is your name?&#34;&#34;) |
-| type | [OpeningMetadata.ApplicationFormQuestion.InputType](#OpeningMetadata.ApplicationFormQuestion.InputType) | required | Suggested type of the UI answer input |
+| question | [string](#string) | optional | The question itself (ie. &#34;What is your name?&#34;&#34;) |
+| type | [OpeningMetadata.ApplicationFormQuestion.InputType](#OpeningMetadata.ApplicationFormQuestion.InputType) | optional | Suggested type of the UI answer input |
 
 
 
 
 
 
-<a name=".WorkingGroupStatusMetadata"></a>
+<a name=".RemoveUpcomingOpening"></a>
 
-### WorkingGroupStatusMetadata
+### RemoveUpcomingOpening
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| id | [string](#string) | optional | Upcoming opening query-node id |
+
+
+
+
+
+
+<a name=".SetGroupMetadata"></a>
+
+### SetGroupMetadata
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| new_metadata | [WorkingGroupMetadata](#WorkingGroupMetadata) | optional | New working group metadata to set (can be a partial update) |
+
+
+
+
+
+
+<a name=".UpcomingOpeningMetadata"></a>
+
+### UpcomingOpeningMetadata
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| expected_start | [uint32](#uint32) | optional | Expected opening start (timestamp) |
+| reward_per_block | [uint64](#uint64) | optional | Expected reward per block |
+| min_application_stake | [uint64](#uint64) | optional | Expected min. application stake |
+| metadata | [OpeningMetadata](#OpeningMetadata) | optional | Opening metadata |
+
+
+
+
+
+
+<a name=".WorkingGroupMetadata"></a>
+
+### WorkingGroupMetadata
 
 
 
@@ -163,6 +280,23 @@
 
 
 
+
+<a name=".WorkingGroupMetadataAction"></a>
+
+### WorkingGroupMetadataAction
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| set_group_metadata | [SetGroupMetadata](#SetGroupMetadata) | optional |  |
+| add_upcoming_opening | [AddUpcomingOpening](#AddUpcomingOpening) | optional |  |
+| remove_upcoming_opening | [RemoveUpcomingOpening](#RemoveUpcomingOpening) | optional |  |
+
+
+
+
+
  
 
 
@@ -173,8 +307,8 @@
 
 | Name | Number | Description |
 | ---- | ------ | ----------- |
+| TEXTAREA | 0 |  |
 | TEXT | 1 |  |
-| TEXTAREA | 2 |  |
 
 
  

+ 1 - 1
metadata-protobuf/package.json

@@ -38,6 +38,6 @@
     "prettier": "2.0.2",
     "ts-node": "^8.8.1",
     "typescript": "^4.1.3",
-    "protobufjs": "^6.10.2"
+    "protobufjs": "^6.11.2"
   }
 }

+ 14 - 0
metadata-protobuf/proto/Forum.proto

@@ -0,0 +1,14 @@
+syntax = "proto2";
+
+// The enum must be wrapped inside "message", otherwide it breaks protobufjs
+message ForumPostReaction {
+  enum Reaction {
+    CANCEL = 0; // This means cancelling any previous reaction
+    LIKE = 1;
+  }
+}
+
+message ForumPostMetadata {
+  optional string text = 1; // Post text content (md-formatted)
+  optional uint32 repliesTo = 2; // Id of the post that given post replies to (if any)
+}

+ 2 - 1
package.json

@@ -45,7 +45,8 @@
     "babel-core": "^7.0.0-bridge.0",
     "typescript": "^3.9.7",
     "bn.js": "^5.1.2",
-    "typeorm": "^0.2.31"
+    "typeorm": "^0.2.31",
+    "pg": "^8.4.0"
   },
   "devDependencies": {
     "eslint": "^7.6.0",

+ 1 - 0
query-node/build.sh

@@ -30,6 +30,7 @@ yarn
 yarn workspace query-node config:dev
 yarn workspace query-node codegen
 sed -i 's/get bytes(): Option/get optBytes(): Option/' ./mappings/generated/types/storage-working-group.ts
+sed -i 's/get categoryId(): Option/get optCategoryId(): Option/' ./mappings/generated/types/forum.ts
 yarn workspace query-node compile
 
 yarn workspace query-node-mappings build

+ 12 - 13
query-node/manifest.yml

@@ -51,27 +51,28 @@ typegen:
     - storageWorkingGroup.RewardPaid
     - storageWorkingGroup.NewMissedRewardLevelReached
   # Forum
-  # FIXME: https://github.com/Joystream/hydra/issues/373
-    # - forum.CategoryCreated
-    # - forum.CategoryUpdated
-    # - forum.CategoryDeleted
-    # - forum.ThreadCreated
-    # - forum.ThreadModerated
-    # - forum.ThreadUpdated
-    # - forum.ThreadTitleUpdated
-    # - forum.ThreadDeleted
-    # - forum.ThreadMoved
+    - forum.CategoryCreated
+    - forum.CategoryUpdated
+    - forum.CategoryDeleted
+    - forum.ThreadCreated
+    - forum.ThreadModerated
+    # - forum.ThreadUpdated FIXME: Not emitted by the runtime
+    - forum.ThreadTitleUpdated
+    - forum.ThreadDeleted
+    - forum.ThreadMoved
+    - forum.VoteOnPoll
+    # FIXME: https://github.com/Joystream/hydra/issues/373
     # - forum.PostAdded
     # - forum.PostModerated
     # - forum.PostDeleted
     # - forum.PostTextUpdated
     # - forum.PostReacted
-    # - forum.VoteOnPoll
     # - forum.CategoryStickyThreadUpdate
     # - forum.CategoryMembershipOfModeratorUpdated
   calls:
     - members.updateProfile
     - members.updateAccounts
+    - forum.createThread
   outDir: ./mappings/generated/types
   customTypes:
     lib: '@joystream/types/augment/all/types'
@@ -307,8 +308,6 @@ mappings:
       handler: forum_ThreadCreated(DatabaseManager, SubstrateEvent)
     - event: forum.ThreadModerated
       handler: forum_ThreadModerated(DatabaseManager, SubstrateEvent)
-    - event: forum.ThreadUpdated
-      handler: forum_ThreadUpdated(DatabaseManager, SubstrateEvent)
     - event: forum.ThreadTitleUpdated
       handler: forum_ThreadTitleUpdated(DatabaseManager, SubstrateEvent)
     - event: forum.ThreadDeleted

+ 6 - 1
query-node/mappings/common.ts

@@ -67,7 +67,12 @@ export async function getOrCreateBlock(
 }
 
 export function bytesToString(b: Bytes): string {
-  return Buffer.from(b.toU8a(true)).toString()
+  return (
+    Buffer.from(b.toU8a(true))
+      .toString()
+      // eslint-disable-next-line no-control-regex
+      .replace(/\u0000/g, '')
+  )
 }
 
 export function hasValuesForProperties<

+ 336 - 15
query-node/mappings/forum.ts

@@ -3,68 +3,388 @@ eslint-disable @typescript-eslint/naming-convention
 */
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
+import BN from 'bn.js'
+import { bytesToString, createEvent } from './common'
+import {
+  CategoryCreatedEvent,
+  CategoryStatusActive,
+  CategoryUpdatedEvent,
+  EventType,
+  ForumCategory,
+  Worker,
+  CategoryStatusArchived,
+  CategoryDeletedEvent,
+  CategoryStatusRemoved,
+  ThreadCreatedEvent,
+  ForumThread,
+  Membership,
+  ThreadStatusActive,
+  ForumPoll,
+  ForumPollAlternative,
+  ThreadModeratedEvent,
+  ThreadStatusModerated,
+  ThreadTitleUpdatedEvent,
+  ThreadDeletedEvent,
+  ThreadStatusLocked,
+  ThreadStatusRemoved,
+  ThreadMovedEvent,
+  ForumPost,
+  PostStatusActive,
+  PostOriginThreadInitial,
+  VoteOnPollEvent,
+} from 'query-node/dist/model'
+import { Forum } from './generated/types'
+import { PrivilegedActor } from '@joystream/types/augment/all'
+
+async function getCategory(db: DatabaseManager, categoryId: string): Promise<ForumCategory> {
+  const category = await db.get(ForumCategory, { where: { id: categoryId } })
+  if (!category) {
+    throw new Error(`Forum category not found by id: ${categoryId}`)
+  }
+
+  return category
+}
+
+async function getThread(db: DatabaseManager, threadId: string): Promise<ForumThread> {
+  const thread = await db.get(ForumThread, { where: { id: threadId } })
+  if (!thread) {
+    throw new Error(`Forum thread not found by id: ${threadId.toString()}`)
+  }
+
+  return thread
+}
+
+async function getPollAlternative(db: DatabaseManager, threadId: string, index: number) {
+  const poll = await db.get(ForumPoll, { where: { thread: { id: threadId } }, relations: ['pollAlternatives'] })
+  if (!poll) {
+    throw new Error(`Forum poll not found by threadId: ${threadId.toString()}`)
+  }
+  const pollAlternative = poll.pollAlternatives?.find((alt) => alt.index === index)
+  if (!pollAlternative) {
+    throw new Error(`Froum poll alternative not found by index ${index} in thread ${threadId.toString()}`)
+  }
+
+  return pollAlternative
+}
+
+async function getActorWorker(db: DatabaseManager, actor: PrivilegedActor): Promise<Worker> {
+  const worker = await db.get(Worker, {
+    where: {
+      group: { id: 'forumWorkingGroup' },
+      ...(actor.isLead ? { isLead: true } : { runtimeId: actor.asModerator.toNumber() }),
+    },
+    relations: ['group'],
+  })
+
+  if (!worker) {
+    throw new Error(`Corresponding worker not found by forum PrivielagedActor: ${JSON.stringify(actor.toHuman())}`)
+  }
+
+  return worker
+}
 
 export async function forum_CategoryCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const {
+    categoryId,
+    optCategoryId: parentCategoryId,
+    bytess: { 0: title, 1: description },
+  } = new Forum.CategoryCreatedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const category = new ForumCategory({
+    id: categoryId.toString(),
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    title: bytesToString(title),
+    description: bytesToString(description),
+    status: new CategoryStatusActive(),
+    parent: parentCategoryId.isSome ? new ForumCategory({ id: parentCategoryId.unwrap().toString() }) : undefined,
+  })
+
+  await db.save<ForumCategory>(category)
+
+  const event = await createEvent(db, event_, EventType.CategoryCreated)
+  const categoryCreatedEvent = new CategoryCreatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    category,
+  })
+  await db.save<CategoryCreatedEvent>(categoryCreatedEvent)
 }
 
 export async function forum_CategoryUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { categoryId, privilegedActor, bool: newArchivalStatus } = new Forum.CategoryUpdatedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const category = await getCategory(db, categoryId.toString())
+  const actorWorker = await getActorWorker(db, privilegedActor)
+
+  const event = await createEvent(db, event_, EventType.CategoryUpdated)
+  const categoryUpdatedEvent = new CategoryUpdatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    category,
+    event,
+    newArchivalStatus: newArchivalStatus.valueOf(),
+    actor: actorWorker,
+  })
+  await db.save<CategoryUpdatedEvent>(categoryUpdatedEvent)
+
+  if (newArchivalStatus.valueOf()) {
+    const status = new CategoryStatusArchived()
+    status.categoryUpdatedEventId = categoryUpdatedEvent.id
+    category.status = status
+  } else {
+    category.status = new CategoryStatusActive()
+  }
+  category.updatedAt = eventTime
+  await db.save<ForumCategory>(category)
 }
 
 export async function forum_CategoryDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { categoryId, privilegedActor } = new Forum.CategoryDeletedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const category = await getCategory(db, categoryId.toString())
+  const actorWorker = await getActorWorker(db, privilegedActor)
+
+  const event = await createEvent(db, event_, EventType.CategoryDeleted)
+  const categoryDeletedEvent = new CategoryDeletedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    category,
+    event,
+    actor: actorWorker,
+  })
+  await db.save<CategoryDeletedEvent>(categoryDeletedEvent)
+
+  const newStatus = new CategoryStatusRemoved()
+  newStatus.categoryDeletedEventId = categoryDeletedEvent.id
+
+  category.updatedAt = eventTime
+  category.status = newStatus
+  await db.save<ForumCategory>(category)
 }
 
 export async function forum_ThreadCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { forumUserId, categoryId, title, text, poll } = new Forum.CreateThreadCall(event_).args
+  const { threadId } = new Forum.ThreadCreatedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const author = new Membership({ id: forumUserId.toString() })
+
+  const thread = new ForumThread({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    id: threadId.toString(),
+    author,
+    category: new ForumCategory({ id: categoryId.toString() }),
+    title: bytesToString(title),
+    isSticky: false,
+    status: new ThreadStatusActive(),
+  })
+  await db.save<ForumThread>(thread)
+
+  if (poll.isSome) {
+    const threadPoll = new ForumPoll({
+      createdAt: eventTime,
+      updatedAt: eventTime,
+      description: bytesToString(poll.unwrap().description_hash), // FIXME: This should be raw description!
+      endTime: new Date(poll.unwrap().end_time.toNumber()),
+      thread,
+    })
+    await db.save<ForumPoll>(threadPoll)
+    await Promise.all(
+      poll.unwrap().poll_alternatives.map(async (alt, index) => {
+        const alternative = new ForumPollAlternative({
+          createdAt: eventTime,
+          updatedAt: eventTime,
+          poll: threadPoll,
+          text: bytesToString(alt.alternative_text_hash), // FIXME: This should be raw text!
+          index,
+        })
+
+        await db.save<ForumPollAlternative>(alternative)
+      })
+    )
+  }
+
+  const event = await createEvent(db, event_, EventType.ThreadCreated)
+  const threadCreatedEvent = new ThreadCreatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    thread,
+    title: bytesToString(title),
+    text: bytesToString(text),
+  })
+  await db.save<ThreadCreatedEvent>(threadCreatedEvent)
+
+  const postOrigin = new PostOriginThreadInitial()
+  postOrigin.threadCreatedEventId = threadCreatedEvent.id
+
+  const initialPost = new ForumPost({
+    // FIXME: The postId is unknown
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    author,
+    thread,
+    text: bytesToString(text),
+    status: new PostStatusActive(),
+    origin: postOrigin,
+  })
+  await db.save<ForumPost>(initialPost)
 }
 
 export async function forum_ThreadModerated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
-}
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { threadId, bytes: rationaleBytes, privilegedActor } = new Forum.ThreadModeratedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const actorWorker = await getActorWorker(db, privilegedActor)
+  const thread = await getThread(db, threadId.toString())
 
-export async function forum_ThreadUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  const event = await createEvent(db, event_, EventType.ThreadModerated)
+  const threadModeratedEvent = new ThreadModeratedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    actor: actorWorker,
+    event,
+    thread,
+    rationale: bytesToString(rationaleBytes),
+  })
+
+  await db.save<ThreadModeratedEvent>(threadModeratedEvent)
+
+  const newStatus = new ThreadStatusModerated()
+  newStatus.threadModeratedEventId = threadModeratedEvent.id
+
+  thread.updatedAt = eventTime
+  thread.status = newStatus
+  await db.save<ForumThread>(thread)
 }
 
 export async function forum_ThreadTitleUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { threadId, bytes: newTitleBytes } = new Forum.ThreadTitleUpdatedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const thread = await getThread(db, threadId.toString())
+
+  const event = await createEvent(db, event_, EventType.ThreadTitleUpdated)
+  const threadTitleUpdatedEvent = new ThreadTitleUpdatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    thread,
+    newTitle: bytesToString(newTitleBytes),
+  })
+
+  await db.save<ThreadTitleUpdatedEvent>(threadTitleUpdatedEvent)
+
+  thread.updatedAt = eventTime
+  thread.title = bytesToString(newTitleBytes)
+  await db.save<ForumThread>(thread)
 }
 
 export async function forum_ThreadDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { threadId, bool: hide } = new Forum.ThreadDeletedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const thread = await getThread(db, threadId.toString())
+
+  const event = await createEvent(db, event_, EventType.ThreadDeleted)
+  const threadDeletedEvent = new ThreadDeletedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    thread,
+  })
+
+  await db.save<ThreadDeletedEvent>(threadDeletedEvent)
+
+  const status = hide.valueOf() ? new ThreadStatusRemoved() : new ThreadStatusLocked()
+  status.threadDeletedEventId = threadDeletedEvent.id
+  thread.status = status
+  thread.updatedAt = eventTime
+  await db.save<ForumThread>(thread)
 }
 
 export async function forum_ThreadMoved(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const {
+    threadId,
+    privilegedActor,
+    categoryIds: { 0: newCategoryId, 1: oldCategoryId },
+  } = new Forum.ThreadMovedEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const thread = await getThread(db, threadId.toString())
+  const actorWorker = await getActorWorker(db, privilegedActor)
+
+  const event = await createEvent(db, event_, EventType.ThreadMoved)
+  const threadMovedEvent = new ThreadMovedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    thread,
+    oldCategory: new ForumCategory({ id: oldCategoryId.toString() }),
+    newCategory: new ForumCategory({ id: newCategoryId.toString() }),
+    actor: actorWorker,
+  })
+
+  await db.save<ThreadMovedEvent>(threadMovedEvent)
+
+  thread.updatedAt = eventTime
+  thread.category = new ForumCategory({ id: newCategoryId.toString() })
+  await db.save<ForumThread>(thread)
+}
+
+export async function forum_VoteOnPoll(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { threadId, forumUserId, u32: alternativeIndex } = new Forum.VoteOnPollEvent(event_).data
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const pollAlternative = await getPollAlternative(db, threadId.toString(), alternativeIndex.toNumber())
+  const votingMember = new Membership({ id: forumUserId.toString() })
+
+  const event = await createEvent(db, event_, EventType.VoteOnPoll)
+  const voteOnPollEvent = new VoteOnPollEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    pollAlternative,
+    votingMember,
+  })
+
+  await db.save<VoteOnPollEvent>(voteOnPollEvent)
 }
 
 export async function forum_PostAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
 export async function forum_PostModerated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
 export async function forum_PostDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
 export async function forum_PostTextUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
 export async function forum_PostReacted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
-}
-
-export async function forum_VoteOnPoll(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
 export async function forum_CategoryStickyThreadUpdate(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }
 
@@ -72,5 +392,6 @@ export async function forum_CategoryMembershipOfModeratorUpdated(
   db: DatabaseManager,
   event_: SubstrateEvent
 ): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   // TODO
 }

+ 1 - 0
query-node/mappings/init.ts

@@ -8,6 +8,7 @@ import path from 'path'
 async function init() {
   const provider = new WsProvider(process.env.WS_PROVIDER_ENDPOINT_URI)
   const api = await ApiPromise.create({ provider, types })
+  // Will be resolved relatively to mappings/lib
   const entitiesPath = path.resolve(__dirname, '../../generated/graphql-server/dist/src/modules/**/*.model.js')
   // We need to create db connection (and configure env) before importing any warthog models
   const dbConnection = await createDBConnection([entitiesPath])

+ 22 - 16
query-node/schemas/forum.graphql

@@ -1,11 +1,3 @@
-type ForumModerator @entity {
-  "Moderator worker"
-  worker: Worker!
-
-  "Categories managed by the moderator"
-  categories: [ForumCategory!] @derivedFrom(field: "moderators")
-}
-
 type CategoryStatusActive @variant {
   # No additional information required
   _phantom: Int
@@ -42,7 +34,7 @@ type ForumCategory @entity {
   threads: [ForumThread!] @derivedFrom(field: "category")
 
   "List of all moderators managing this category"
-  moderators: [ForumModerator!]
+  moderators: [Worker!]
 
   "The event the category was created in"
   createdInEvent: CategoryCreatedEvent! @derivedFrom(field: "category")
@@ -76,7 +68,7 @@ type ThreadStatusModerated @variant {
 type ThreadStatusRemoved @variant {
   # TODO: Variant relationship
   "Id of the event the thread was removed in"
-  threadDeletedEvent: ID!
+  threadDeletedEventId: ID!
 }
 
 union ThreadStatus = ThreadStatusActive | ThreadStatusLocked | ThreadStatusModerated | ThreadStatusRemoved
@@ -94,9 +86,6 @@ type ForumThread @entity {
   "Thread title"
   title: String! @fulltext(query: "threadsByTitle")
 
-  "Thread text (md-formatted)"
-  text: String! @fulltext(query: "threadsByText")
-
   "All posts in the thread"
   posts: [ForumPost!] @derivedFrom(field: "thread")
 
@@ -107,7 +96,7 @@ type ForumThread @entity {
   isSticky: Boolean!
 
   "The event the thread was created in"
-  createdInEvent: ThreadCreatedEvent!
+  createdInEvent: ThreadCreatedEvent! @derivedFrom(field: "thread")
 
   "Current thread status"
   status: ThreadStatus!
@@ -135,6 +124,9 @@ type ForumPoll @entity {
 }
 
 type ForumPollAlternative @entity {
+  "Index uniquely identifying the alternative in given poll"
+  index: Int!
+
   "The related poll"
   poll: ForumPoll!
 
@@ -193,6 +185,20 @@ type PostStatusRemoved @variant {
 
 union PostStatus = PostStatusActive | PostStatusLocked | PostStatusModerated | PostStatusRemoved
 
+type PostOriginThreadInitial @variant {
+  # TODO: Variant relationship
+  "Id of the related thread creation event"
+  threadCreatedEventId: ID!
+}
+
+type PostOriginThreadReply @variant {
+  # TODO: Variant relationship
+  "Id of the related post added event"
+  postAddedEventId: ID!
+}
+
+union PostOrigin = PostOriginThreadInitial | PostOriginThreadReply
+
 type ForumPost @entity {
   "Runtime post id"
   id: ID!
@@ -212,8 +218,8 @@ type ForumPost @entity {
   "Current post status"
   status: PostStatus!
 
-  "The event the post was created in"
-  createdInEvent: PostAddedEvent! @derivedFrom(field: "post")
+  "The origin of the post (either thread creation event or regular PostAdded event)"
+  origin: PostOrigin!
 
   "List of all text update events (edits)"
   edits: [PostTextUpdatedEvent!] @derivedFrom(field: "post")

+ 15 - 10
query-node/schemas/forumEvents.graphql

@@ -5,8 +5,7 @@ type CategoryCreatedEvent @entity {
   "The created category"
   category: ForumCategory!
 
-  "The moderator (possibly lead) responsible for creating the category"
-  actor: ForumModerator!
+  # The actor is always lead
 }
 
 type CategoryUpdatedEvent @entity {
@@ -20,7 +19,7 @@ type CategoryUpdatedEvent @entity {
   newArchivalStatus: Boolean!
 
   "The moderator (possibly lead) responsible for updating the category"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 type CategoryDeletedEvent @entity {
@@ -31,7 +30,7 @@ type CategoryDeletedEvent @entity {
   category: ForumCategory!
 
   "The moderator (possibly lead) responsible for deleting the category"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 type ThreadCreatedEvent @entity {
@@ -41,6 +40,12 @@ type ThreadCreatedEvent @entity {
   "The thread that was created"
   thread: ForumThread!
 
+  "Thread's original title"
+  title: String!
+
+  "Thread's original text"
+  text: String!
+
   # The author is already part of the Thread entity itself and is immutable
 }
 
@@ -55,7 +60,7 @@ type ThreadModeratedEvent @entity {
   rationale: String!
 
   "Actor responsible for the moderation"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 # FIXME: Not emitted by the runtime
@@ -70,7 +75,7 @@ type ThreadModeratedEvent @entity {
 #   newArchivalStatus: Boolean!
 
 #   "Actor responsible for the update"
-#   actor: ForumModerator!
+#   actor: Worker!
 # }
 
 type ThreadTitleUpdatedEvent @entity {
@@ -110,7 +115,7 @@ type ThreadMovedEvent @entity {
   newCategory: ForumCategory!
 
   "The actor performing the transfer"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 type PostAddedEvent @entity {
@@ -138,7 +143,7 @@ type PostModeratedEvent @entity {
   rationale: String!
 
   "The actor responsible for the moderation"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 type PostDeletedEvent @entity {
@@ -201,7 +206,7 @@ type CategoryStickyThreadUpdateEvent @entity {
   newStickyThreads: [ForumThread!]
 
   "The actor responsible for making the threads sticky"
-  actor: ForumModerator!
+  actor: Worker!
 }
 
 type CategoryMembershipOfModeratorUpdated @entity {
@@ -209,7 +214,7 @@ type CategoryMembershipOfModeratorUpdated @entity {
   event: Event!
 
   "The moderator in question"
-  moderator: ForumModerator!
+  moderator: Worker!
 
   "The category in question"
   category: ForumCategory!

+ 3 - 0
query-node/schemas/workingGroups.graphql

@@ -76,6 +76,9 @@ type Worker @entity {
 
   "Worker's storage data"
   storage: String
+
+  "Forum categories managed by the worker (required for many-to-many relationship with ForumCategory)"
+  managedForumCategories: [ForumCategory!] @derivedFrom(field: "moderators")
 }
 
 type WorkingGroupMetadata @entity {

+ 38 - 1
tests/integration-tests/src/Api.ts

@@ -2,7 +2,7 @@ import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'
 import { u32, BTreeMap } from '@polkadot/types'
 import { ISubmittableResult } from '@polkadot/types/types'
 import { KeyringPair } from '@polkadot/keyring/types'
-import { AccountId, MemberId } from '@joystream/types/common'
+import { AccountId, MemberId, PostId, ThreadId } from '@joystream/types/common'
 
 import { AccountInfo, Balance, EventRecord, BlockNumber, BlockHash } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
@@ -24,6 +24,10 @@ import {
   WorkingGroupModuleName,
   AppliedOnOpeningEventDetails,
   OpeningFilledEventDetails,
+  ForumEventName,
+  CategoryCreatedEventDetails,
+  PostAddedEventDetails,
+  ThreadCreatedEventDetails,
 } from './types'
 import {
   ApplicationId,
@@ -35,6 +39,7 @@ import {
 } from '@joystream/types/working-group'
 import { DeriveAllSections } from '@polkadot/api/util/decorate'
 import { ExactDerive } from '@polkadot/api-derive'
+import { CategoryId } from '@joystream/types/forum'
 
 export enum WorkingGroups {
   StorageWorkingGroup = 'storageWorkingGroup',
@@ -439,4 +444,36 @@ export class Api {
   public async getLeaderStakingKey(group: WorkingGroupModuleName): Promise<string> {
     return (await this.getLeader(group)).staking_account_id.toString()
   }
+
+  public async retrieveForumEventDetails(result: ISubmittableResult, eventName: ForumEventName): Promise<EventDetails> {
+    const details = await this.retrieveEventDetails(result, 'forum', eventName)
+    if (!details) {
+      throw new Error(`${eventName} event details not found in result: ${JSON.stringify(result.toHuman())}`)
+    }
+    return details
+  }
+
+  public async retrieveCategoryCreatedEventDetails(result: ISubmittableResult): Promise<CategoryCreatedEventDetails> {
+    const details = await this.retrieveForumEventDetails(result, 'CategoryCreated')
+    return {
+      ...details,
+      categoryId: details.event.data[0] as CategoryId,
+    }
+  }
+
+  public async retrieveThreadCreatedEventDetails(result: ISubmittableResult): Promise<ThreadCreatedEventDetails> {
+    const details = await this.retrieveForumEventDetails(result, 'ThreadCreated')
+    return {
+      ...details,
+      threadId: details.event.data[0] as ThreadId,
+    }
+  }
+
+  public async retrievePostAddedEventDetails(result: ISubmittableResult): Promise<PostAddedEventDetails> {
+    const details = await this.retrieveForumEventDetails(result, 'PostAdded')
+    return {
+      ...details,
+      postId: details.event.data[0] as PostId,
+    }
+  }
 }

+ 103 - 3
tests/integration-tests/src/QueryNodeApi.ts

@@ -1,5 +1,5 @@
 import { ApolloClient, DocumentNode, NormalizedCacheObject } from '@apollo/client'
-import { MemberId } from '@joystream/types/common'
+import { MemberId, ThreadId } from '@joystream/types/common'
 import Debugger from 'debug'
 import { ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { EventDetails, WorkingGroupModuleName } from './types'
@@ -172,9 +172,42 @@ import {
   GetLeaderSetEventsByEventIdsQuery,
   GetLeaderSetEventsByEventIdsQueryVariables,
   GetLeaderSetEventsByEventIds,
+  ForumCategoryFieldsFragment,
+  GetCategoriesByIdsQuery,
+  GetCategoriesByIdsQueryVariables,
+  GetCategoriesByIds,
+  CategoryCreatedEventFieldsFragment,
+  GetCategoryCreatedEventsByEventIdsQuery,
+  GetCategoryCreatedEventsByEventIdsQueryVariables,
+  GetCategoryCreatedEventsByEventIds,
+  CategoryUpdatedEventFieldsFragment,
+  GetCategoryUpdatedEventsByEventIdsQuery,
+  GetCategoryUpdatedEventsByEventIdsQueryVariables,
+  GetCategoryUpdatedEventsByEventIds,
+  CategoryDeletedEventFieldsFragment,
+  GetCategoryDeletedEventsByEventIdsQuery,
+  GetCategoryDeletedEventsByEventIdsQueryVariables,
+  GetCategoryDeletedEventsByEventIds,
+  ThreadCreatedEventFieldsFragment,
+  GetThreadCreatedEventsByEventIdsQuery,
+  GetThreadCreatedEventsByEventIds,
+  GetThreadCreatedEventsByEventIdsQueryVariables,
+  VoteOnPollEventFieldsFragment,
+  GetVoteOnPollEventsByEventIdsQuery,
+  GetVoteOnPollEventsByEventIdsQueryVariables,
+  GetVoteOnPollEventsByEventIds,
+  ThreadDeletedEventFieldsFragment,
+  GetThreadDeletedEventsByEventIdsQuery,
+  GetThreadDeletedEventsByEventIdsQueryVariables,
+  GetThreadDeletedEventsByEventIds,
+  ForumThreadWithPostsFieldsFragment,
+  GetThreadsWithPostsByIdsQuery,
+  GetThreadsWithPostsByIdsQueryVariables,
+  GetThreadsWithPostsByIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
+import { CategoryId } from '@joystream/types/forum'
 export class QueryNodeApi {
   private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
   private readonly debug: Debugger.Debugger
@@ -199,7 +232,9 @@ export class QueryNodeApi {
     const failDebug = this.tryDebug.extend(label).extend('failed')
     return new Promise((resolve, reject) => {
       let lastError: any
+      let retryTimeout: NodeJS.Timeout
       const timeout = setTimeout(() => {
+        clearTimeout(retryTimeout)
         failDebug(`Query node query is still failing after timeout was reached (${timeoutMs}ms)!`)
         reject(lastError)
       }, timeoutMs)
@@ -217,13 +252,13 @@ export class QueryNodeApi {
                 }, retyring query in ${retryTimeMs}ms...`
               )
               lastError = e
-              setTimeout(tryQuery, retryTimeMs)
+              retryTimeout = setTimeout(tryQuery, retryTimeMs)
             }
           })
           .catch((e) => {
             retryDebug(`Query node unreachable, retyring query in ${retryTimeMs}ms...`)
             lastError = e
-            setTimeout(tryQuery, retryTimeMs)
+            retryTimeout = setTimeout(tryQuery, retryTimeMs)
           })
       }
 
@@ -663,4 +698,69 @@ export class QueryNodeApi {
       'leaderUnsetEvents'
     )
   }
+
+  public async getCategoriesByIds(ids: CategoryId[]): Promise<ForumCategoryFieldsFragment[]> {
+    return this.multipleEntitiesQuery<GetCategoriesByIdsQuery, GetCategoriesByIdsQueryVariables>(
+      GetCategoriesByIds,
+      { ids: ids.map((id) => id.toString()) },
+      'forumCategories'
+    )
+  }
+
+  public async getCategoryCreatedEvents(events: EventDetails[]): Promise<CategoryCreatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetCategoryCreatedEventsByEventIdsQuery,
+      GetCategoryCreatedEventsByEventIdsQueryVariables
+    >(GetCategoryCreatedEventsByEventIds, { eventIds }, 'categoryCreatedEvents')
+  }
+
+  public async getCategoryUpdatedEvents(events: EventDetails[]): Promise<CategoryUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetCategoryUpdatedEventsByEventIdsQuery,
+      GetCategoryUpdatedEventsByEventIdsQueryVariables
+    >(GetCategoryUpdatedEventsByEventIds, { eventIds }, 'categoryUpdatedEvents')
+  }
+
+  public async getCategoryDeletedEvents(events: EventDetails[]): Promise<CategoryDeletedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetCategoryDeletedEventsByEventIdsQuery,
+      GetCategoryDeletedEventsByEventIdsQueryVariables
+    >(GetCategoryDeletedEventsByEventIds, { eventIds }, 'categoryDeletedEvents')
+  }
+
+  public async getThreadCreatedEvents(events: EventDetails[]): Promise<ThreadCreatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetThreadCreatedEventsByEventIdsQuery,
+      GetThreadCreatedEventsByEventIdsQueryVariables
+    >(GetThreadCreatedEventsByEventIds, { eventIds }, 'threadCreatedEvents')
+  }
+
+  public async getThreadsWithPostsByIds(ids: ThreadId[]): Promise<ForumThreadWithPostsFieldsFragment[]> {
+    return this.multipleEntitiesQuery<GetThreadsWithPostsByIdsQuery, GetThreadsWithPostsByIdsQueryVariables>(
+      GetThreadsWithPostsByIds,
+      { ids: ids.map((id) => id.toString()) },
+      'forumThreads'
+    )
+  }
+
+  public async getVoteOnPollEvents(events: EventDetails[]): Promise<VoteOnPollEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<GetVoteOnPollEventsByEventIdsQuery, GetVoteOnPollEventsByEventIdsQueryVariables>(
+      GetVoteOnPollEventsByEventIds,
+      { eventIds },
+      'voteOnPollEvents'
+    )
+  }
+
+  public async getThreadDeletedEvents(events: EventDetails[]): Promise<ThreadDeletedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetThreadDeletedEventsByEventIdsQuery,
+      GetThreadDeletedEventsByEventIdsQueryVariables
+    >(GetThreadDeletedEventsByEventIds, { eventIds }, 'threadDeletedEvents')
+  }
 }

+ 2 - 0
tests/integration-tests/src/consts.ts

@@ -5,6 +5,8 @@ export const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
 export const MIN_APPLICATION_STAKE = new BN(2000)
 export const MIN_UNSTANKING_PERIOD = 43201
 export const LEADER_OPENING_STAKE = new BN(2000)
+export const THREAD_DEPOSIT = new BN(30)
+export const POST_DEPOSIT = new BN(10)
 
 export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
   storageWorkingGroup: '0x0606060606060606',

+ 82 - 0
tests/integration-tests/src/fixtures/forum/CreateCategoriesFixture.ts

@@ -0,0 +1,82 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { CategoryCreatedEventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { CategoryCreatedEventFieldsFragment, ForumCategoryFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { CategoryId } from '@joystream/types/forum'
+
+export type CategoryParams = {
+  title: string
+  description: string
+  parentId?: CategoryId
+}
+
+export class CreateCategoriesFixture extends StandardizedFixture {
+  protected events: CategoryCreatedEventDetails[] = []
+
+  protected categoriesParams: CategoryParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, categoriesParams: CategoryParams[]) {
+    super(api, query)
+    this.categoriesParams = categoriesParams
+  }
+
+  public getCreatedCategoriesIds(): CategoryId[] {
+    if (!this.events.length) {
+      throw new Error('Trying to get created categories ids before they were created!')
+    }
+    return this.events.map((e) => e.categoryId)
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return await this.api.getLeadRoleKey('forumWorkingGroup')
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.categoriesParams.map((params) =>
+      this.api.tx.forum.createCategory(params.parentId || null, params.title, params.description)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<CategoryCreatedEventDetails> {
+    return this.api.retrieveCategoryCreatedEventDetails(result)
+  }
+
+  protected assertQueriedCategoriesAreValid(qCategories: ForumCategoryFieldsFragment[]): void {
+    this.events.map((e, i) => {
+      const qCategory = qCategories.find((c) => c.id === e.categoryId.toString())
+      const categoryParams = this.categoriesParams[i]
+      Utils.assert(qCategory, 'Query node: Category not found')
+      assert.equal(qCategory.description, categoryParams.description)
+      assert.equal(qCategory.title, categoryParams.title)
+      if (categoryParams.parentId) {
+        Utils.assert(qCategory.parent, 'Query node: Category parent was expected, but not set')
+        assert.equal(qCategory.parent.id, categoryParams.parentId.toString())
+      }
+      assert.equal(qCategory.status.__typename, 'CategoryStatusActive')
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: CategoryCreatedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.CategoryCreated)
+    assert.equal(qEvent.category.id, this.events[i].categoryId.toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getCategoryCreatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the categories
+    const qCategories = await this.query.getCategoriesByIds(this.events.map((e) => e.categoryId))
+    this.assertQueriedCategoriesAreValid(qCategories)
+  }
+}

+ 139 - 0
tests/integration-tests/src/fixtures/forum/CreateThreadsFixture.ts

@@ -0,0 +1,139 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { ThreadCreatedEventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { ForumThreadWithPostsFieldsFragment, ThreadCreatedEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType, PostOriginThreadInitial } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { CategoryId, Poll } from '@joystream/types/forum'
+import { MemberId, ThreadId } from '@joystream/types/common'
+import { CreateInterface } from '@joystream/types'
+import { POST_DEPOSIT, THREAD_DEPOSIT } from '../../consts'
+
+export type PollParams = {
+  description: string
+  endTime: Date
+  alternatives: string[]
+}
+
+export type ThreadParams = {
+  title: string
+  text: string
+  categoryId: CategoryId
+  asMember: MemberId
+  poll?: PollParams
+}
+
+export class CreateThreadsFixture extends StandardizedFixture {
+  protected events: ThreadCreatedEventDetails[] = []
+
+  protected threadsParams: ThreadParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, threadsParams: ThreadParams[]) {
+    super(api, query)
+    this.threadsParams = threadsParams
+  }
+
+  public getCreatedThreadsIds(): ThreadId[] {
+    if (!this.events.length) {
+      throw new Error('Trying to get created threads ids before they were created!')
+    }
+    return this.events.map((e) => e.threadId)
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return await Promise.all(
+      this.threadsParams.map(async ({ asMember }) =>
+        (await this.api.query.members.membershipById(asMember)).controller_account.toString()
+      )
+    )
+  }
+
+  public async execute(): Promise<void> {
+    const accounts = await this.getSignerAccountOrAccounts()
+    // Send required funds to accounts (ThreadDeposit + PostDeposit)
+    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, THREAD_DEPOSIT.add(POST_DEPOSIT))))
+    await super.execute()
+  }
+
+  protected parsePollParams(pollParams?: PollParams): CreateInterface<Poll> | null {
+    if (!pollParams) {
+      return null
+    }
+
+    return {
+      description_hash: pollParams.description,
+      end_time: pollParams.endTime.getTime(),
+      poll_alternatives: pollParams.alternatives.map((a) => ({
+        alternative_text_hash: a,
+        vote_count: 0,
+      })),
+    }
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.threadsParams.map((params) =>
+      this.api.tx.forum.createThread(
+        params.asMember,
+        params.categoryId,
+        params.title,
+        params.text,
+        this.parsePollParams(params.poll)
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<ThreadCreatedEventDetails> {
+    return this.api.retrieveThreadCreatedEventDetails(result)
+  }
+
+  protected assertQueriedThreadsAreValid(
+    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qEvents: ThreadCreatedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qThread = qThreads.find((t) => t.id === e.threadId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const threadParams = this.threadsParams[i]
+      Utils.assert(qThread, 'Query node: Thread not found')
+      assert.equal(qThread.title, threadParams.title)
+      assert.equal(qThread.category.id, threadParams.categoryId.toString())
+      assert.equal(qThread.author.id, threadParams.asMember.toString())
+      assert.equal(qThread.status.__typename, 'ThreadStatusActive')
+      assert.equal(qThread.isSticky, false)
+      assert.equal(qThread.createdInEvent.id, qEvent.id)
+      const initialPost = qThread.posts.find((p) => p.origin.__typename === 'PostOriginThreadInitial')
+      Utils.assert(initialPost, "Query node: Thread's initial post not found!")
+      assert.equal(initialPost.text, threadParams.text)
+      assert.equal((initialPost.origin as PostOriginThreadInitial).threadCreatedEventId, qEvent.id)
+      assert.equal(initialPost.author.id, threadParams.asMember.toString())
+      assert.equal(initialPost.status.__typename, 'PostStatusActive')
+      if (threadParams.poll) {
+        Utils.assert(qThread.poll, 'Query node: Thread poll is missing')
+        assert.equal(qThread.poll.description, threadParams.poll.description)
+        assert.equal(new Date(qThread.poll.endTime).getTime(), threadParams.poll.endTime.getTime())
+      }
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ThreadCreatedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.ThreadCreated)
+    assert.equal(qEvent.thread.id, this.events[i].threadId.toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getThreadCreatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the threads
+    const qThreads = await this.query.getThreadsWithPostsByIds(this.events.map((e) => e.threadId))
+    this.assertQueriedThreadsAreValid(qThreads, qEvents)
+  }
+}

+ 96 - 0
tests/integration-tests/src/fixtures/forum/DeleteThreadsFixture.ts

@@ -0,0 +1,96 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, MemberContext } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { ForumThreadWithPostsFieldsFragment, ThreadDeletedEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { CategoryId } from '@joystream/types/forum'
+import { ThreadId } from '@joystream/types/common'
+
+export type ThreadRemovalInput = {
+  threadId: ThreadId
+  categoryId: CategoryId
+  hide?: boolean // defaults to "true"
+}
+
+export class DeleteThreadsFixture extends StandardizedFixture {
+  protected removals: ThreadRemovalInput[]
+  protected threadAuthors: MemberContext[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, removals: ThreadRemovalInput[]) {
+    super(api, query)
+    this.removals = removals
+  }
+
+  protected async loadAuthors(): Promise<void> {
+    this.threadAuthors = await Promise.all(
+      this.removals.map(async (r) => {
+        const thread = await this.api.query.forum.threadById(r.categoryId, r.threadId)
+        const member = await this.api.query.members.membershipById(thread.author_id)
+        return { account: member.controller_account.toString(), memberId: thread.author_id }
+      })
+    )
+  }
+
+  public async execute(): Promise<void> {
+    await this.loadAuthors()
+    await super.execute()
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.threadAuthors.map((a) => a.account)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.removals.map((r, i) =>
+      this.api.tx.forum.deleteThread(
+        this.threadAuthors[i].memberId,
+        r.categoryId,
+        r.threadId,
+        r.hide === undefined ? true : r.hide
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'ThreadDeleted')
+  }
+
+  protected assertQueriedThreadsAreValid(
+    qThreads: ForumThreadWithPostsFieldsFragment[],
+    qEvents: ThreadDeletedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const removal = this.removals[i]
+      const hidden = removal.hide === undefined ? true : removal.hide
+      const expectedStatus = hidden ? 'ThreadStatusRemoved' : 'ThreadStatusLocked'
+      const qThread = qThreads.find((t) => t.id === removal.threadId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      Utils.assert(qThread, 'Query node: Thread not found')
+      Utils.assert(qThread.status.__typename === expectedStatus, `Invalid thread status. Expected: ${expectedStatus}`)
+      assert.equal(qThread.status.threadDeletedEventId, qEvent.id)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ThreadDeletedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.ThreadDeleted)
+    assert.equal(qEvent.thread.id, this.removals[i].threadId.toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getThreadDeletedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the threads
+    const qThreads = await this.query.getThreadsWithPostsByIds(this.removals.map((r) => r.threadId))
+    this.assertQueriedThreadsAreValid(qThreads, qEvents)
+  }
+}

+ 79 - 0
tests/integration-tests/src/fixtures/forum/RemoveCategoriesFixture.ts

@@ -0,0 +1,79 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { CategoryDeletedEventFieldsFragment, ForumCategoryFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { CategoryId } from '@joystream/types/forum'
+import { WithForumLeadFixture } from './WithForumLeadFixture'
+
+export type CategoryRemovalInput = {
+  categoryId: CategoryId
+  asWorker?: WorkerId
+}
+
+export class RemoveCategoriesFixture extends WithForumLeadFixture {
+  protected removals: CategoryRemovalInput[]
+
+  public constructor(api: Api, query: QueryNodeApi, removals: CategoryRemovalInput[]) {
+    super(api, query)
+    this.removals = removals
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return Promise.all(
+      this.removals.map(async (r) => {
+        const workerId = r.asWorker || (await this.getForumLeadId())
+        return (await this.api.query.forumWorkingGroup.workerById(workerId)).role_account_id.toString()
+      })
+    )
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.removals.map((u) =>
+      this.api.tx.forum.deleteCategory(u.asWorker ? { Moderator: u.asWorker } : { Lead: null }, u.categoryId)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'CategoryDeleted')
+  }
+
+  protected assertQueriedCategoriesAreValid(
+    qCategories: ForumCategoryFieldsFragment[],
+    qEvents: CategoryDeletedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const removal = this.removals[i]
+      const qCategory = qCategories.find((c) => c.id === removal.categoryId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      Utils.assert(qCategory, 'Query node: Category not found')
+      Utils.assert(qCategory.status.__typename === 'CategoryStatusRemoved', 'Invalid category status')
+      assert.equal(qCategory.status.categoryDeletedEventId, qEvent.id)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: CategoryDeletedEventFieldsFragment, i: number): void {
+    const { categoryId, asWorker } = this.removals[i]
+    assert.equal(qEvent.event.type, EventType.CategoryDeleted)
+    assert.equal(qEvent.category.id, categoryId.toString())
+    assert.equal(qEvent.actor.id, `forumWorkingGroup-${asWorker ? asWorker.toString() : this.forumLeadId!.toString()}`)
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getCategoryDeletedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the categories
+    const qCategories = await this.query.getCategoriesByIds(this.removals.map((r) => r.categoryId))
+    this.assertQueriedCategoriesAreValid(qCategories, qEvents)
+  }
+}

+ 89 - 0
tests/integration-tests/src/fixtures/forum/UpdateCategoriesStatusFixture.ts

@@ -0,0 +1,89 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { CategoryUpdatedEventFieldsFragment, ForumCategoryFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { CategoryId } from '@joystream/types/forum'
+import { WithForumLeadFixture } from './WithForumLeadFixture'
+
+export type CategoryStatusUpdate = {
+  categoryId: CategoryId
+  archived: boolean
+  asWorker?: WorkerId
+}
+
+export class UpdateCategoriesStatusFixture extends WithForumLeadFixture {
+  protected updates: CategoryStatusUpdate[]
+
+  public constructor(api: Api, query: QueryNodeApi, updates: CategoryStatusUpdate[]) {
+    super(api, query)
+    this.updates = updates
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return Promise.all(
+      this.updates.map(async (u) => {
+        const workerId = u.asWorker || (await this.getForumLeadId())
+        return (await this.api.query.forumWorkingGroup.workerById(workerId)).role_account_id.toString()
+      })
+    )
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.updates.map((u) =>
+      this.api.tx.forum.updateCategoryArchivalStatus(
+        u.asWorker ? { Moderator: u.asWorker } : { Lead: null },
+        u.categoryId,
+        u.archived
+      )
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'CategoryUpdated')
+  }
+
+  protected assertQueriedCategoriesAreValid(
+    qCategories: ForumCategoryFieldsFragment[],
+    qEvents: CategoryUpdatedEventFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const update = this.updates[i]
+      const qCategory = qCategories.find((c) => c.id === update.categoryId.toString())
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      Utils.assert(qCategory, 'Query node: Category not found')
+      if (update.archived) {
+        Utils.assert(qCategory.status.__typename === 'CategoryStatusArchived', 'Invalid category status')
+        assert.equal(qCategory.status.categoryUpdatedEventId, qEvent.id)
+      } else {
+        assert.equal(qCategory.status.__typename, 'CategoryStatusActive')
+      }
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: CategoryUpdatedEventFieldsFragment, i: number): void {
+    const { categoryId, archived, asWorker } = this.updates[i]
+    assert.equal(qEvent.event.type, EventType.CategoryUpdated)
+    assert.equal(qEvent.category.id, categoryId.toString())
+    assert.equal(qEvent.newArchivalStatus, archived)
+    assert.equal(qEvent.actor.id, `forumWorkingGroup-${asWorker ? asWorker.toString() : this.forumLeadId!.toString()}`)
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getCategoryUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the categories
+    const qCategories = await this.query.getCategoriesByIds(this.updates.map((u) => u.categoryId))
+    this.assertQueriedCategoriesAreValid(qCategories, qEvents)
+  }
+}

+ 78 - 0
tests/integration-tests/src/fixtures/forum/VoteOnPollFixture.ts

@@ -0,0 +1,78 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { ForumThreadWithPostsFieldsFragment, VoteOnPollEventFieldsFragment } from '../../graphql/generated/queries'
+import { EventType } from '../../graphql/generated/schema'
+import { assert } from 'chai'
+import { StandardizedFixture } from '../../Fixture'
+import { CategoryId } from '@joystream/types/forum'
+import { MemberId, ThreadId } from '@joystream/types/common'
+import { Utils } from '../../utils'
+import _ from 'lodash'
+
+export type VoteParams = {
+  categoryId: CategoryId
+  threadId: ThreadId
+  index: number
+  asMember: MemberId
+}
+
+export class VoteOnPollFixture extends StandardizedFixture {
+  protected votes: VoteParams[]
+
+  public constructor(api: Api, query: QueryNodeApi, votes: VoteParams[]) {
+    super(api, query)
+    this.votes = votes
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return await Promise.all(
+      this.votes.map(async ({ asMember }) =>
+        (await this.api.query.members.membershipById(asMember)).controller_account.toString()
+      )
+    )
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.votes.map((params) =>
+      this.api.tx.forum.voteOnPoll(params.asMember, params.categoryId, params.threadId, params.index)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'VoteOnPoll')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: VoteOnPollEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.VoteOnPoll)
+    assert.equal(qEvent.pollAlternative.poll.thread.id, this.votes[i].threadId.toString())
+    assert.equal(qEvent.pollAlternative.index, this.votes[i].index)
+    assert.equal(qEvent.votingMember.id, this.votes[i].asMember.toString())
+  }
+
+  protected assertQueriedThreadsAreValid(qThreads: ForumThreadWithPostsFieldsFragment[]): void {
+    this.votes.forEach(({ asMember, threadId, index }) => {
+      const qThread = qThreads.find((t) => t.id === threadId.toString())
+      Utils.assert(qThread, 'Query node: Thread not found')
+      Utils.assert(
+        qThread.poll?.pollAlternatives[index].votes.find((v) => v.votingMember.id === asMember.toString()),
+        'Query node: Member vote not found'
+      )
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getVoteOnPollEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the threads
+    const qThreads = await this.query.getThreadsWithPostsByIds(this.votes.map((v) => v.threadId))
+    this.assertQueriedThreadsAreValid(qThreads)
+  }
+}

+ 19 - 0
tests/integration-tests/src/fixtures/forum/WithForumLeadFixture.ts

@@ -0,0 +1,19 @@
+import { WorkerId } from '@joystream/types/working-group'
+import { StandardizedFixture } from '../../Fixture'
+
+export abstract class WithForumLeadFixture extends StandardizedFixture {
+  protected forumLeadId?: WorkerId
+
+  protected async getForumLeadId(): Promise<WorkerId> {
+    if (!this.forumLeadId) {
+      const optForumLeadId = await this.api.query.forumWorkingGroup.currentLead()
+      if (optForumLeadId.isNone) {
+        throw new Error('Forum working group lead not set!')
+      }
+
+      this.forumLeadId = optForumLeadId.unwrap()
+    }
+
+    return this.forumLeadId
+  }
+}

+ 6 - 0
tests/integration-tests/src/fixtures/forum/index.ts

@@ -0,0 +1,6 @@
+export { CreateCategoriesFixture, CategoryParams } from './CreateCategoriesFixture'
+export { UpdateCategoriesStatusFixture, CategoryStatusUpdate } from './UpdateCategoriesStatusFixture'
+export { RemoveCategoriesFixture, CategoryRemovalInput } from './RemoveCategoriesFixture'
+export { CreateThreadsFixture, ThreadParams } from './CreateThreadsFixture'
+export { DeleteThreadsFixture, ThreadRemovalInput } from './DeleteThreadsFixture'
+export { VoteOnPollFixture, VoteParams } from './VoteOnPollFixture'

+ 86 - 0
tests/integration-tests/src/flows/forum/categories.ts

@@ -0,0 +1,86 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import {
+  CategoryParams,
+  CreateCategoriesFixture,
+  CategoryStatusUpdate,
+  UpdateCategoriesStatusFixture,
+  RemoveCategoriesFixture,
+} from '../../fixtures/forum'
+
+export default async function categories({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger(`flow:cateogries`)
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  // Create root categories
+  const categories: CategoryParams[] = [
+    { title: 'General', description: 'General stuff' },
+    { title: 'Working Groups', description: 'Working groups related discussions' },
+  ]
+
+  const createCategoriesFixture = new CreateCategoriesFixture(api, query, categories)
+  const createCategoriesRunner = new FixtureRunner(createCategoriesFixture)
+  await createCategoriesRunner.run()
+  const rootCategoryIds = createCategoriesFixture.getCreatedCategoriesIds()
+  const workingGroupsCategoryId = rootCategoryIds[1]
+
+  // Create subcategories
+  const workingGroupsSubcategories: CategoryParams[] = [
+    {
+      title: 'Forum Working Group',
+      description: 'Forum Working Group related discussions',
+      parentId: workingGroupsCategoryId,
+    },
+    {
+      title: 'Storage Working Group',
+      description: 'Storage Working Group related discussions',
+      parentId: workingGroupsCategoryId,
+    },
+    {
+      title: 'Membership Working Group',
+      description: 'Membership Working Group related discussions',
+      parentId: workingGroupsCategoryId,
+    },
+  ]
+  const createSubcategoriesFixture = new CreateCategoriesFixture(api, query, workingGroupsSubcategories)
+  const createSubcategoriesRunner = new FixtureRunner(createSubcategoriesFixture)
+  await createSubcategoriesRunner.run()
+  const subcategoryIds = createSubcategoriesFixture.getCreatedCategoriesIds()
+
+  await Promise.all([createCategoriesRunner.runQueryNodeChecks(), createSubcategoriesRunner.runQueryNodeChecks()])
+
+  // Update archival status
+  const categoryUpdatesArchival: CategoryStatusUpdate[] = subcategoryIds.map((id) => ({
+    categoryId: id,
+    archived: true,
+  }))
+
+  const categoryUpdatesArchivalFixture = new UpdateCategoriesStatusFixture(api, query, categoryUpdatesArchival)
+  await new FixtureRunner(categoryUpdatesArchivalFixture).runWithQueryNodeChecks()
+
+  const categoryUpdatesActive: CategoryStatusUpdate[] = categoryUpdatesArchival.map((u) => ({ ...u, archived: false }))
+
+  const categoryUpdatesActiveFixture = new UpdateCategoriesStatusFixture(api, query, categoryUpdatesActive)
+  await new FixtureRunner(categoryUpdatesActiveFixture).runWithQueryNodeChecks()
+
+  // Remove categories (make sure subcategories are removed first)
+  const removeSubcategoriesFixture = new RemoveCategoriesFixture(
+    api,
+    query,
+    subcategoryIds.map((categoryId) => ({ categoryId }))
+  )
+  const removeRootCategoriesFixture = new RemoveCategoriesFixture(
+    api,
+    query,
+    rootCategoryIds.map((categoryId) => ({ categoryId }))
+  )
+  const removeSubcategoriesRunner = new FixtureRunner(removeSubcategoriesFixture)
+  const removeRootCategoriesRunner = new FixtureRunner(removeRootCategoriesFixture)
+  await removeSubcategoriesRunner.run()
+  await removeRootCategoriesRunner.run()
+  await Promise.all([removeSubcategoriesRunner.runQueryNodeChecks(), removeRootCategoriesRunner.runQueryNodeChecks()])
+
+  debug('Done')
+}

+ 72 - 0
tests/integration-tests/src/flows/forum/polls.ts

@@ -0,0 +1,72 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import {
+  CategoryParams,
+  CreateCategoriesFixture,
+  VoteParams,
+  CreateThreadsFixture,
+  ThreadParams,
+  VoteOnPollFixture,
+} from '../../fixtures/forum'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+
+export default async function polls({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger(`flow:polls`)
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  // Create test member(s)
+  const accounts = (await api.createKeyPairs(5)).map((kp) => kp.address)
+  const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
+  await new FixtureRunner(buyMembershipFixture).run()
+  const memberIds = buyMembershipFixture.getCreatedMembers()
+
+  // Create some test category first
+  const categories: CategoryParams[] = [{ title: 'Polls', description: 'Testing the polls' }]
+  const createCategoriesFixture = new CreateCategoriesFixture(api, query, categories)
+  await new FixtureRunner(createCategoriesFixture).run()
+  const [categoryId] = createCategoriesFixture.getCreatedCategoriesIds()
+
+  // Create polls
+  const pollThreads: ThreadParams[] = memberIds.map((memberId, i) => ({
+    categoryId,
+    asMember: memberId,
+    title: `Poll ${i}`,
+    text: `Poll ${i} desc`,
+    poll: {
+      description: `Poll ${i} question?`,
+      alternatives: [`${i}:A`, `${i}:B`, `${i}:C`],
+      endTime: new Date(Date.now() + (i + 1) * 60 * 60 * 1000), // +(i+1) hours
+    },
+  }))
+
+  const createThreadsFixture = new CreateThreadsFixture(api, query, pollThreads)
+  const createThreadsRunner = new FixtureRunner(createThreadsFixture)
+  await createThreadsRunner.run()
+  const pollThreadIds = createThreadsFixture.getCreatedThreadsIds()
+
+  // Vote on polls
+  const votes: VoteParams[] = pollThreadIds.reduce(
+    (votesArray, threadId) =>
+      votesArray.concat(
+        memberIds.map((memberId, i) => {
+          const index = i % 3
+          return {
+            threadId,
+            categoryId,
+            asMember: memberId,
+            index,
+          }
+        })
+      ),
+    [] as VoteParams[]
+  )
+  const voteOnPollFixture = new VoteOnPollFixture(api, query, votes)
+  const voteOnPollRunner = new FixtureRunner(voteOnPollFixture)
+  await voteOnPollRunner.run()
+
+  await Promise.all([createThreadsRunner.runQueryNodeChecks(), voteOnPollRunner.runQueryNodeChecks()])
+
+  debug('Done')
+}

+ 59 - 0
tests/integration-tests/src/flows/forum/threads.ts

@@ -0,0 +1,59 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { CategoryParams, CreateCategoriesFixture, DeleteThreadsFixture, ThreadRemovalInput } from '../../fixtures/forum'
+import { CreateThreadsFixture, ThreadParams } from '../../fixtures/forum/CreateThreadsFixture'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+
+export default async function threads({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger(`flow:threads`)
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  // Create test member(s)
+  const accounts = (await api.createKeyPairs(5)).map((kp) => kp.address)
+  const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
+  await new FixtureRunner(buyMembershipFixture).run()
+  const memberIds = buyMembershipFixture.getCreatedMembers()
+
+  // Create some test categories first
+  const categories: CategoryParams[] = [
+    { title: 'Test 1', description: 'Test category 1' },
+    { title: 'Test 2', description: 'Test category 2' },
+  ]
+  const createCategoriesFixture = new CreateCategoriesFixture(api, query, categories)
+  await new FixtureRunner(createCategoriesFixture).run()
+  const categoryIds = createCategoriesFixture.getCreatedCategoriesIds()
+
+  // Create threads
+  const threads: ThreadParams[] = categoryIds.reduce(
+    (threadsArray, categoryId) =>
+      threadsArray.concat(
+        memberIds.map((memberId) => ({
+          categoryId,
+          asMember: memberId,
+          title: `Thread ${categoryId}/${memberId}`,
+          text: `Example thread of member ${memberId.toString()} in category ${categoryId.toString()}`,
+        }))
+      ),
+    [] as ThreadParams[]
+  )
+
+  const createThreadsFixture = new CreateThreadsFixture(api, query, threads)
+  const createThreadsRunner = new FixtureRunner(createThreadsFixture)
+  await createThreadsRunner.runWithQueryNodeChecks()
+  const threadIds = createThreadsFixture.getCreatedThreadsIds()
+
+  // TODO: Threads updates
+
+  // Remove threads
+  const threadRemovals: ThreadRemovalInput[] = threads.map((t, i) => ({
+    threadId: threadIds[i],
+    categoryId: t.categoryId,
+    hide: i >= 1, // Test both cases
+  }))
+  const removeThreadsFixture = new DeleteThreadsFixture(api, query, threadRemovals)
+  await new FixtureRunner(removeThreadsFixture).runWithQueryNodeChecks()
+
+  debug('Done')
+}

+ 386 - 16
tests/integration-tests/src/graphql/generated/queries.ts

@@ -11,6 +11,134 @@ export type EventFieldsFragment = {
   inBlock: BlockFieldsFragment
 }
 
+export type ForumCategoryFieldsFragment = {
+  id: string
+  title: string
+  description: string
+  parent?: Types.Maybe<{ id: string }>
+  threads: Array<{ id: string }>
+  moderators: Array<{ id: string }>
+  createdInEvent: { id: string }
+  status:
+    | { __typename: 'CategoryStatusActive' }
+    | { __typename: 'CategoryStatusArchived'; categoryUpdatedEventId: string }
+    | { __typename: 'CategoryStatusRemoved'; categoryDeletedEventId: string }
+}
+
+export type GetCategoriesByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetCategoriesByIdsQuery = { forumCategories: Array<ForumCategoryFieldsFragment> }
+
+export type ForumThreadWithPostsFieldsFragment = {
+  id: string
+  title: string
+  isSticky: boolean
+  author: { id: string }
+  category: { id: string }
+  posts: Array<{
+    id: string
+    text: string
+    author: { id: string }
+    status:
+      | { __typename: 'PostStatusActive' }
+      | { __typename: 'PostStatusLocked' }
+      | { __typename: 'PostStatusModerated' }
+      | { __typename: 'PostStatusRemoved' }
+    origin:
+      | { __typename: 'PostOriginThreadInitial'; threadCreatedEventId: string }
+      | { __typename: 'PostOriginThreadReply'; postAddedEventId: string }
+  }>
+  poll?: Types.Maybe<{
+    description: string
+    endTime: any
+    pollAlternatives: Array<{ index: number; text: string; votes: Array<{ votingMember: { id: string } }> }>
+  }>
+  createdInEvent: { id: string }
+  status:
+    | { __typename: 'ThreadStatusActive' }
+    | { __typename: 'ThreadStatusLocked'; threadDeletedEventId: string }
+    | { __typename: 'ThreadStatusModerated'; threadModeratedEventId: string }
+    | { __typename: 'ThreadStatusRemoved'; threadDeletedEventId: string }
+}
+
+export type GetThreadsWithPostsByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetThreadsWithPostsByIdsQuery = { forumThreads: Array<ForumThreadWithPostsFieldsFragment> }
+
+export type CategoryCreatedEventFieldsFragment = { id: string; event: EventFieldsFragment; category: { id: string } }
+
+export type GetCategoryCreatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetCategoryCreatedEventsByEventIdsQuery = {
+  categoryCreatedEvents: Array<CategoryCreatedEventFieldsFragment>
+}
+
+export type CategoryUpdatedEventFieldsFragment = {
+  id: string
+  newArchivalStatus: boolean
+  event: EventFieldsFragment
+  category: { id: string }
+  actor: { id: string }
+}
+
+export type GetCategoryUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetCategoryUpdatedEventsByEventIdsQuery = {
+  categoryUpdatedEvents: Array<CategoryUpdatedEventFieldsFragment>
+}
+
+export type CategoryDeletedEventFieldsFragment = {
+  id: string
+  event: EventFieldsFragment
+  category: { id: string }
+  actor: { id: string }
+}
+
+export type GetCategoryDeletedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetCategoryDeletedEventsByEventIdsQuery = {
+  categoryDeletedEvents: Array<CategoryDeletedEventFieldsFragment>
+}
+
+export type ThreadCreatedEventFieldsFragment = { id: string; event: EventFieldsFragment; thread: { id: string } }
+
+export type GetThreadCreatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetThreadCreatedEventsByEventIdsQuery = { threadCreatedEvents: Array<ThreadCreatedEventFieldsFragment> }
+
+export type VoteOnPollEventFieldsFragment = {
+  id: string
+  event: EventFieldsFragment
+  pollAlternative: { id: string; index: number; text: string; poll: { thread: { id: string } } }
+  votingMember: { id: string }
+}
+
+export type GetVoteOnPollEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetVoteOnPollEventsByEventIdsQuery = { voteOnPollEvents: Array<VoteOnPollEventFieldsFragment> }
+
+export type ThreadDeletedEventFieldsFragment = { id: string; event: EventFieldsFragment; thread: { id: string } }
+
+export type GetThreadDeletedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetThreadDeletedEventsByEventIdsQuery = { threadDeletedEvents: Array<ThreadDeletedEventFieldsFragment> }
+
 export type MemberMetadataFieldsFragment = { name?: Types.Maybe<string>; about?: Types.Maybe<string> }
 
 export type MembershipFieldsFragment = {
@@ -714,10 +842,93 @@ export type GetBudgetSpendingEventsByEventIdsQueryVariables = Types.Exact<{
 
 export type GetBudgetSpendingEventsByEventIdsQuery = { budgetSpendingEvents: Array<BudgetSpendingEventFieldsFragment> }
 
-export const MemberMetadataFields = gql`
-  fragment MemberMetadataFields on MemberMetadata {
-    name
-    about
+export const ForumCategoryFields = gql`
+  fragment ForumCategoryFields on ForumCategory {
+    id
+    parent {
+      id
+    }
+    title
+    description
+    threads {
+      id
+    }
+    moderators {
+      id
+    }
+    createdInEvent {
+      id
+    }
+    status {
+      __typename
+      ... on CategoryStatusArchived {
+        categoryUpdatedEventId
+      }
+      ... on CategoryStatusRemoved {
+        categoryDeletedEventId
+      }
+    }
+  }
+`
+export const ForumThreadWithPostsFields = gql`
+  fragment ForumThreadWithPostsFields on ForumThread {
+    id
+    author {
+      id
+    }
+    category {
+      id
+    }
+    title
+    posts {
+      id
+      text
+      author {
+        id
+      }
+      text
+      status {
+        __typename
+      }
+      origin {
+        __typename
+        ... on PostOriginThreadInitial {
+          threadCreatedEventId
+        }
+        ... on PostOriginThreadReply {
+          postAddedEventId
+        }
+      }
+    }
+    poll {
+      description
+      endTime
+      pollAlternatives {
+        index
+        text
+        votes {
+          votingMember {
+            id
+          }
+        }
+      }
+    }
+    isSticky
+    createdInEvent {
+      id
+    }
+    status {
+      __typename
+      ... on ThreadStatusLocked {
+        threadDeletedEventId
+      }
+      ... on ThreadStatusModerated {
+        threadModeratedEventId
+      }
+      ... on ThreadStatusRemoved {
+        threadDeletedEventId
+      }
+    }
   }
 `
 export const BlockFields = gql`
@@ -727,6 +938,113 @@ export const BlockFields = gql`
     network
   }
 `
+export const EventFields = gql`
+  fragment EventFields on Event {
+    id
+    inBlock {
+      ...BlockFields
+    }
+    inExtrinsic
+    indexInBlock
+    type
+  }
+  ${BlockFields}
+`
+export const CategoryCreatedEventFields = gql`
+  fragment CategoryCreatedEventFields on CategoryCreatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    category {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const CategoryUpdatedEventFields = gql`
+  fragment CategoryUpdatedEventFields on CategoryUpdatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    category {
+      id
+    }
+    newArchivalStatus
+    actor {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const CategoryDeletedEventFields = gql`
+  fragment CategoryDeletedEventFields on CategoryDeletedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    category {
+      id
+    }
+    actor {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const ThreadCreatedEventFields = gql`
+  fragment ThreadCreatedEventFields on ThreadCreatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    thread {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const VoteOnPollEventFields = gql`
+  fragment VoteOnPollEventFields on VoteOnPollEvent {
+    id
+    event {
+      ...EventFields
+    }
+    pollAlternative {
+      id
+      index
+      text
+      poll {
+        thread {
+          id
+        }
+      }
+    }
+    votingMember {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const ThreadDeletedEventFields = gql`
+  fragment ThreadDeletedEventFields on ThreadDeletedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    thread {
+      id
+    }
+  }
+  ${EventFields}
+`
+export const MemberMetadataFields = gql`
+  fragment MemberMetadataFields on MemberMetadata {
+    name
+    about
+  }
+`
 export const MembershipFields = gql`
   fragment MembershipFields on Membership {
     id
@@ -767,18 +1085,6 @@ export const MembershipSystemSnapshotFields = gql`
   }
   ${BlockFields}
 `
-export const EventFields = gql`
-  fragment EventFields on Event {
-    id
-    inBlock {
-      ...BlockFields
-    }
-    inExtrinsic
-    indexInBlock
-    type
-  }
-  ${BlockFields}
-`
 export const MembershipBoughtEventFields = gql`
   fragment MembershipBoughtEventFields on MembershipBoughtEvent {
     id
@@ -1494,6 +1800,70 @@ export const BudgetSpendingEventFields = gql`
   }
   ${EventFields}
 `
+export const GetCategoriesByIds = gql`
+  query getCategoriesByIds($ids: [ID!]) {
+    forumCategories(where: { id_in: $ids }) {
+      ...ForumCategoryFields
+    }
+  }
+  ${ForumCategoryFields}
+`
+export const GetThreadsWithPostsByIds = gql`
+  query getThreadsWithPostsByIds($ids: [ID!]) {
+    forumThreads(where: { id_in: $ids }) {
+      ...ForumThreadWithPostsFields
+    }
+  }
+  ${ForumThreadWithPostsFields}
+`
+export const GetCategoryCreatedEventsByEventIds = gql`
+  query getCategoryCreatedEventsByEventIds($eventIds: [ID!]) {
+    categoryCreatedEvents(where: { eventId_in: $eventIds }) {
+      ...CategoryCreatedEventFields
+    }
+  }
+  ${CategoryCreatedEventFields}
+`
+export const GetCategoryUpdatedEventsByEventIds = gql`
+  query getCategoryUpdatedEventsByEventIds($eventIds: [ID!]) {
+    categoryUpdatedEvents(where: { eventId_in: $eventIds }) {
+      ...CategoryUpdatedEventFields
+    }
+  }
+  ${CategoryUpdatedEventFields}
+`
+export const GetCategoryDeletedEventsByEventIds = gql`
+  query getCategoryDeletedEventsByEventIds($eventIds: [ID!]) {
+    categoryDeletedEvents(where: { eventId_in: $eventIds }) {
+      ...CategoryDeletedEventFields
+    }
+  }
+  ${CategoryDeletedEventFields}
+`
+export const GetThreadCreatedEventsByEventIds = gql`
+  query getThreadCreatedEventsByEventIds($eventIds: [ID!]) {
+    threadCreatedEvents(where: { eventId_in: $eventIds }) {
+      ...ThreadCreatedEventFields
+    }
+  }
+  ${ThreadCreatedEventFields}
+`
+export const GetVoteOnPollEventsByEventIds = gql`
+  query getVoteOnPollEventsByEventIds($eventIds: [ID!]) {
+    voteOnPollEvents(where: { eventId_in: $eventIds }) {
+      ...VoteOnPollEventFields
+    }
+  }
+  ${VoteOnPollEventFields}
+`
+export const GetThreadDeletedEventsByEventIds = gql`
+  query getThreadDeletedEventsByEventIds($eventIds: [ID!]) {
+    threadDeletedEvents(where: { eventId_in: $eventIds }) {
+      ...ThreadDeletedEventFields
+    }
+  }
+  ${ThreadDeletedEventFields}
+`
 export const GetMemberById = gql`
   query getMemberById($id: ID!) {
     membershipByUniqueInput(where: { id: $id }) {

File diff suppressed because it is too large
+ 358 - 391
tests/integration-tests/src/graphql/generated/schema.ts


+ 98 - 0
tests/integration-tests/src/graphql/queries/forum.graphql

@@ -0,0 +1,98 @@
+fragment ForumCategoryFields on ForumCategory {
+  id
+  parent {
+    id
+  }
+  title
+  description
+  threads {
+    id
+  }
+  moderators {
+    id
+  }
+  createdInEvent {
+    id
+  }
+  status {
+    __typename
+    ... on CategoryStatusArchived {
+      categoryUpdatedEventId
+    }
+    ... on CategoryStatusRemoved {
+      categoryDeletedEventId
+    }
+  }
+}
+
+query getCategoriesByIds($ids: [ID!]) {
+  forumCategories(where: { id_in: $ids }) {
+    ...ForumCategoryFields
+  }
+}
+
+fragment ForumThreadWithPostsFields on ForumThread {
+  id
+  author {
+    id
+  }
+  category {
+    id
+  }
+  title
+  posts {
+    id
+    text
+    author {
+      id
+    }
+    text
+    status {
+      __typename
+    }
+    origin {
+      __typename
+      ... on PostOriginThreadInitial {
+        threadCreatedEventId
+      }
+      ... on PostOriginThreadReply {
+        postAddedEventId
+      }
+    }
+  }
+  poll {
+    description
+    endTime
+    pollAlternatives {
+      index
+      text
+      votes {
+        votingMember {
+          id
+        }
+      }
+    }
+  }
+  isSticky
+  createdInEvent {
+    id
+  }
+  status {
+    __typename
+    ... on ThreadStatusLocked {
+      threadDeletedEventId
+    }
+    ... on ThreadStatusModerated {
+      threadModeratedEventId
+    }
+    ... on ThreadStatusRemoved {
+      threadDeletedEventId
+    }
+  }
+}
+
+query getThreadsWithPostsByIds($ids: [ID!]) {
+  forumThreads(where: { id_in: $ids }) {
+    ...ForumThreadWithPostsFields
+  }
+}

+ 112 - 0
tests/integration-tests/src/graphql/queries/forumEvents.graphql

@@ -0,0 +1,112 @@
+fragment CategoryCreatedEventFields on CategoryCreatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  category {
+    id
+  }
+}
+
+query getCategoryCreatedEventsByEventIds($eventIds: [ID!]) {
+  categoryCreatedEvents(where: { eventId_in: $eventIds }) {
+    ...CategoryCreatedEventFields
+  }
+}
+
+fragment CategoryUpdatedEventFields on CategoryUpdatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  category {
+    id
+  }
+  newArchivalStatus
+  actor {
+    id
+  }
+}
+
+query getCategoryUpdatedEventsByEventIds($eventIds: [ID!]) {
+  categoryUpdatedEvents(where: { eventId_in: $eventIds }) {
+    ...CategoryUpdatedEventFields
+  }
+}
+
+fragment CategoryDeletedEventFields on CategoryDeletedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  category {
+    id
+  }
+  actor {
+    id
+  }
+}
+
+query getCategoryDeletedEventsByEventIds($eventIds: [ID!]) {
+  categoryDeletedEvents(where: { eventId_in: $eventIds }) {
+    ...CategoryDeletedEventFields
+  }
+}
+
+fragment ThreadCreatedEventFields on ThreadCreatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  thread {
+    id
+  }
+}
+
+query getThreadCreatedEventsByEventIds($eventIds: [ID!]) {
+  threadCreatedEvents(where: { eventId_in: $eventIds }) {
+    ...ThreadCreatedEventFields
+  }
+}
+
+fragment VoteOnPollEventFields on VoteOnPollEvent {
+  id
+  event {
+    ...EventFields
+  }
+  pollAlternative {
+    id
+    index
+    text
+    poll {
+      thread {
+        id
+      }
+    }
+  }
+  votingMember {
+    id
+  }
+}
+
+query getVoteOnPollEventsByEventIds($eventIds: [ID!]) {
+  voteOnPollEvents(where: { eventId_in: $eventIds }) {
+    ...VoteOnPollEventFields
+  }
+}
+
+fragment ThreadDeletedEventFields on ThreadDeletedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  thread {
+    id
+  }
+}
+
+query getThreadDeletedEventsByEventIds($eventIds: [ID!]) {
+  threadDeletedEvents(where: { eventId_in: $eventIds }) {
+    ...ThreadDeletedEventFields
+  }
+}

+ 12 - 0
tests/integration-tests/src/scenarios/forum.ts

@@ -0,0 +1,12 @@
+import categories from '../flows/forum/categories'
+import polls from '../flows/forum/polls'
+import threads from '../flows/forum/threads'
+import leadOpening from '../flows/working-groups/leadOpening'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  const sudoHireLead = job('hiring working group leads', leadOpening)
+  job('forum categories', categories).requires(sudoHireLead)
+  job('forum threads', threads).requires(sudoHireLead)
+  job('forum polls', polls).requires(sudoHireLead)
+})

+ 40 - 2
tests/integration-tests/src/scenarios/full.ts

@@ -1,2 +1,40 @@
-import './memberships'
-import './workingGroups'
+import categories from '../flows/forum/categories'
+import polls from '../flows/forum/polls'
+import threads from '../flows/forum/threads'
+import leadOpening from '../flows/working-groups/leadOpening'
+import creatingMemberships from '../flows/membership/creatingMemberships'
+import updatingMemberProfile from '../flows/membership/updatingProfile'
+import updatingMemberAccounts from '../flows/membership/updatingAccounts'
+import invitingMebers from '../flows/membership/invitingMembers'
+import transferringInvites from '../flows/membership/transferringInvites'
+import managingStakingAccounts from '../flows/membership/managingStakingAccounts'
+import membershipSystem from '../flows/membership/membershipSystem'
+import openingsAndApplications from '../flows/working-groups/openingsAndApplications'
+import upcomingOpenings from '../flows/working-groups/upcomingOpenings'
+import groupStatus from '../flows/working-groups/groupStatus'
+import workerActions from '../flows/working-groups/workerActions'
+import groupBudget from '../flows/working-groups/groupBudget'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  // Membership:
+  const membershipSystemJob = job('membership system', membershipSystem)
+  // All other membership jobs should be executed after, otherwise the varying membershipPrice etc. may break them
+  job('creating members', creatingMemberships).after(membershipSystemJob)
+  job('updating member profile', updatingMemberProfile).after(membershipSystemJob)
+  job('updating member accounts', updatingMemberAccounts).after(membershipSystemJob)
+  job('inviting members', invitingMebers).after(membershipSystemJob)
+  job('transferring invites', transferringInvites).after(membershipSystemJob)
+  job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
+  // Woring groups:
+  const sudoHireLead = job('sudo lead opening', leadOpening)
+  job('openings and applications', openingsAndApplications).requires(sudoHireLead)
+  job('upcoming openings', upcomingOpenings).requires(sudoHireLead)
+  job('group status', groupStatus).requires(sudoHireLead)
+  job('worker actions', workerActions).requires(sudoHireLead)
+  job('group budget', groupBudget).requires(sudoHireLead)
+  // Forum:
+  job('forum categories', categories).requires(sudoHireLead)
+  job('forum threads', threads).requires(sudoHireLead)
+  job('forum polls', polls).requires(sudoHireLead)
+})

+ 44 - 7
tests/integration-tests/src/types.ts

@@ -1,16 +1,11 @@
-import { MemberId } from '@joystream/types/common'
+import { MemberId, PostId, ThreadId } from '@joystream/types/common'
 import { ApplicationId, OpeningId, WorkerId, ApplyOnOpeningParameters } from '@joystream/types/working-group'
 import { Event } from '@polkadot/types/interfaces/system'
 import { BTreeMap } from '@polkadot/types'
 import { EventFieldsFragment } from './graphql/generated/queries'
-
-export type MemberContext = {
-  account: string
-  memberId: MemberId
-}
+import { CategoryId } from '@joystream/types/forum'
 
 export type AnyQueryNodeEvent = { event: EventFieldsFragment }
-
 export interface EventDetails {
   event: Event
   blockNumber: number
@@ -19,6 +14,13 @@ export interface EventDetails {
   indexInBlock: number
 }
 
+export type MemberContext = {
+  account: string
+  memberId: MemberId
+}
+
+// Membership
+
 export interface MembershipBoughtEventDetails extends EventDetails {
   memberId: MemberId
 }
@@ -43,6 +45,8 @@ export type MembershipEventName =
   | 'InitialInvitationBalanceUpdated'
   | 'LeaderInvitationQuotaUpdated'
 
+// Working groups
+
 export interface OpeningAddedEventDetails extends EventDetails {
   openingId: OpeningId
 }
@@ -85,3 +89,36 @@ export type WorkingGroupModuleName =
   | 'contentDirectoryWorkingGroup'
   | 'forumWorkingGroup'
   | 'membershipWorkingGroup'
+
+// Forum
+
+export interface CategoryCreatedEventDetails extends EventDetails {
+  categoryId: CategoryId
+}
+
+export interface ThreadCreatedEventDetails extends EventDetails {
+  threadId: ThreadId
+}
+
+export interface PostAddedEventDetails extends EventDetails {
+  postId: PostId
+}
+
+export type ForumEventName =
+  | 'CategoryCreated'
+  | 'CategoryUpdated'
+  | 'CategoryDeleted'
+  | 'ThreadCreated'
+  | 'ThreadModerated'
+  | 'ThreadUpdated'
+  | 'ThreadTitleUpdated'
+  | 'ThreadDeleted'
+  | 'ThreadMoved'
+  | 'PostAdded'
+  | 'PostModerated'
+  | 'PostDeleted'
+  | 'PostTextUpdated'
+  | 'PostReacted'
+  | 'VoteOnPoll'
+  | 'CategoryStickyThreadUpdate'
+  | 'CategoryMembershipOfModeratorUpdated'

+ 33 - 97
yarn.lock

@@ -5367,6 +5367,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
   integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
 
+"@types/node@>=13.7.0":
+  version "15.0.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.3.tgz#ee09fcaac513576474c327da5818d421b98db88a"
+  integrity sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==
+
 "@types/node@^10.1.0", "@types/node@^10.17.18":
   version "10.17.50"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.50.tgz#7a20902af591282aa9176baefc37d4372131c32d"
@@ -5377,11 +5382,6 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.11.tgz#9220ab4b20d91169eb78f456dbfcbabee89dfb50"
   integrity sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w==
 
-"@types/node@^13.7.0":
-  version "13.13.51"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.51.tgz#a424c5282f99fc1ca41f11b727b6aea80668bcba"
-  integrity sha512-66/xg5I5Te4oGi5Jws11PtNmKkZbOPZWyBZZ/l5AOrWj1Dyw+6Ge/JhYTq/2/Yvdqyhrue8RL+DGI298OJ0xcg==
-
 "@types/node@^9.6.4":
   version "9.6.61"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.61.tgz#29f124eddd41c4c74281bd0b455d689109fc2a2d"
@@ -8524,11 +8524,6 @@ buffer-json@^2.0.0:
   resolved "https://registry.yarnpkg.com/buffer-json/-/buffer-json-2.0.0.tgz#f73e13b1e42f196fe2fd67d001c7d7107edd7c23"
   integrity sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==
 
-buffer-writer@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-1.0.1.tgz#22a936901e3029afcd7547eb4487ceb697a3bf08"
-  integrity sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=
-
 buffer-writer@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
@@ -13824,11 +13819,6 @@ gaze@^1.0.0:
   dependencies:
     globule "^1.0.0"
 
-generic-pool@2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
-  integrity sha1-eAw29p360FpaBF3Te+etyhGk9v8=
-
 genfun@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
@@ -17883,11 +17873,6 @@ js-sha3@^0.8.0, js-sha3@~0.8.0:
   resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
   integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
 
-js-string-escape@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
-  integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
-
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -21068,11 +21053,6 @@ oauth-sign@~0.9.0:
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
-object-assign@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
-  integrity sha1-ejs9DpgGPUP0wD8uiubNUahog6A=
-
 object-assign@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa"
@@ -21705,11 +21685,6 @@ package-json@^6.3.0:
     registry-url "^5.0.0"
     semver "^6.2.0"
 
-packet-reader@0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-0.3.1.tgz#cd62e60af8d7fea8a705ec4ff990871c46871f27"
-  integrity sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc=
-
 packet-reader@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
@@ -22076,11 +22051,16 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-pg-connection-string@0.1.3, pg-connection-string@^0.1.3:
+pg-connection-string@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
   integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=
 
+pg-connection-string@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
+  integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
+
 pg-format@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/pg-format/-/pg-format-1.0.4.tgz#27734236c2ad3f4e5064915a59334e20040a828e"
@@ -22100,34 +22080,15 @@ pg-listen@^1.7.0:
     pg-format "^1.0.4"
     typed-emitter "^0.1.0"
 
-pg-packet-stream@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz#e45c3ae678b901a2873af1e17b92d787962ef914"
-  integrity sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg==
-
-pg-pool@1.*:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
-  integrity sha1-9+xzgkw3oD8Hb1G/33DjQBR8Tzc=
-  dependencies:
-    generic-pool "2.4.3"
-    object-assign "4.1.0"
-
-pg-pool@^2.0.10:
-  version "2.0.10"
-  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.10.tgz#842ee23b04e86824ce9d786430f8365082d81c4a"
-  integrity sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg==
+pg-pool@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.3.0.tgz#12d5c7f65ea18a6e99ca9811bd18129071e562fc"
+  integrity sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==
 
-pg-types@1.*:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
-  integrity sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ==
-  dependencies:
-    pg-int8 "1.0.1"
-    postgres-array "~1.0.0"
-    postgres-bytea "~1.0.0"
-    postgres-date "~1.0.0"
-    postgres-interval "^1.1.0"
+pg-protocol@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0"
+  integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==
 
 pg-types@^2.1.0:
   version "2.2.0"
@@ -22140,35 +22101,20 @@ pg-types@^2.1.0:
     postgres-date "~1.0.4"
     postgres-interval "^1.1.0"
 
-pg@^6.1.0:
-  version "6.4.2"
-  resolved "https://registry.yarnpkg.com/pg/-/pg-6.4.2.tgz#c364011060eac7a507a2ae063eb857ece910e27f"
-  integrity sha1-w2QBEGDqx6UHoq4GPrhX7OkQ4n8=
-  dependencies:
-    buffer-writer "1.0.1"
-    js-string-escape "1.0.1"
-    packet-reader "0.3.1"
-    pg-connection-string "0.1.3"
-    pg-pool "1.*"
-    pg-types "1.*"
-    pgpass "1.*"
-    semver "4.3.2"
-
-pg@^7.12.1:
-  version "7.18.2"
-  resolved "https://registry.yarnpkg.com/pg/-/pg-7.18.2.tgz#4e219f05a00aff4db6aab1ba02f28ffa4513b0bb"
-  integrity sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA==
+pg@^6.1.0, pg@^7.12.1, pg@^8.4.0:
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/pg/-/pg-8.6.0.tgz#e222296b0b079b280cce106ea991703335487db2"
+  integrity sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==
   dependencies:
     buffer-writer "2.0.0"
     packet-reader "1.0.0"
-    pg-connection-string "0.1.3"
-    pg-packet-stream "^1.1.0"
-    pg-pool "^2.0.10"
+    pg-connection-string "^2.5.0"
+    pg-pool "^3.3.0"
+    pg-protocol "^1.5.0"
     pg-types "^2.1.0"
     pgpass "1.x"
-    semver "4.3.2"
 
-pgpass@1.*, pgpass@1.x:
+pgpass@1.x:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.4.tgz#85eb93a83800b20f8057a2b029bf05abaf94ea9c"
   integrity sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==
@@ -22729,11 +22675,6 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
-postgres-array@~1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5"
-  integrity sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ==
-
 postgres-array@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
@@ -22744,7 +22685,7 @@ postgres-bytea@~1.0.0:
   resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
   integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
 
-postgres-date@~1.0.0, postgres-date@~1.0.4:
+postgres-date@~1.0.4:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
   integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
@@ -23081,10 +23022,10 @@ proto-list@~1.2.1:
   resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
   integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
 
-protobufjs@^6.10.2:
-  version "6.10.2"
-  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b"
-  integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ==
+protobufjs@^6.11.2:
+  version "6.11.2"
+  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
+  integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
   dependencies:
     "@protobufjs/aspromise" "^1.1.2"
     "@protobufjs/base64" "^1.1.2"
@@ -23097,7 +23038,7 @@ protobufjs@^6.10.2:
     "@protobufjs/pool" "^1.1.0"
     "@protobufjs/utf8" "^1.1.0"
     "@types/long" "^4.0.1"
-    "@types/node" "^13.7.0"
+    "@types/node" ">=13.7.0"
     long "^4.0.0"
 
 protocol-buffers-schema@^3.3.1:
@@ -25155,11 +25096,6 @@ semver-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@4.3.2:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
-  integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=
-
 semver@7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"

Some files were not shown because too many files changed in this diff