Developers
Extensions
Task Metadata Resolvers

Task Metadata Resolvers

Task metadata resolvers use the tasks extension to check if a task has metadata, resolve remote metadata and return or update the task with that metadata. They make it easy to request data that might be incomplete or not yet populated on the frontend.

The resolver itself is defined as an interface with three functions, getKey that returns a <Key>, hasMetadata as a boolean if a task is passed in and resolve as a promise if a task is passed in.

export interface Resolver<Key, T> {
    getKey(): Key;
 
    hasMetadata(task: BaseTask): boolean;
 
    resolve(task: BaseTask): Promise<T | undefined>;
}

in /tasks/resolvers (opens in a new tab) there is three resolver files,

  • project-resolver - that returns ProjectTaskMetadata
  • subject-resolver - that returns a snippet of data for a canvas, manifest or collection
  • selector-thumbnail - that returns a svg as a string

Looking at selector-thumbnail, we can see its returning selectorThumbnail as the key, checking if there is metadata (in this case a thumbnail image) and if not resolving it with api.getProjectSVG

import { ApiClient } from '../../../gateway/api';
import { BaseTask } from '../../../gateway/tasks/base-task';
import { Resolver } from './resolver';
 
export type SelectorThumbnail = {
  svg: string;
};
 
export class SelectorThumbnailResolver implements Resolver<'selectorThumbnail', SelectorThumbnail | null> {
  api: ApiClient;
  constructor(api: ApiClient) {
    this.api = api;
  }
 
  getKey() {
    return 'selectorThumbnail' as const;
  }
 
  hasMetadata(task: BaseTask) {
    const metadata = task.metadata;
 
    if (!task.root_task) {
      return true;
    }
 
    if (task.type !== 'crowdsourcing-task') {
      return true;
    }
 
    if (task.status !== 3) {
      return true;
    }
 
    if (!metadata) {
      return false;
    }
 
    if (typeof metadata.selectorThumbnail === 'undefined' || metadata.selectorThumbnail === null) {
      return false;
    }
 
    // Otherwise it should be up-to-date.
    return true;
  }
 
  async resolve(task: BaseTask) {
    try {
      if (!task.id) {
        return null;
      }
 
      const resp = await this.api.getProjectSVG('any', task.id);
 
      if (!resp || resp.empty) {
        return null;
      }
 
      return {
        svg: resp.svg,
      };
    } catch (e) {
      console.log('error', e);
      return null;
    }
  }
}

To step through how the resolver is working in more detail, starting on the frontend where a crowdsourcing task is passed into a hook that returns metadata

const metadata = useTaskMetadata<{ subject?: SubjectSnippet }>(task);

that in turn calls getMetadata from the tasks extension,

return api.tasks.getMetadata(task);

getMetadata will first check if the task has metadata using requiresUpdate, if the metadata exists it returns the existing metadata,

  async getMetadata<T extends BaseTask>(task: T) {
    if (this.requiresUpdate(task)) {
      // then fetch from server.
      return await this.api.publicRequest<any>(`/madoc/api/task-metadata/${task.id}`);
    }
 
    return task.metadata;
  }

Otherwise, the task-metadata endpoint used will do a couple of things. It will fetch the remote metadata also defined in the tasks extension then it will update the task with the new metadata! So next time metadata is requested from the frontend it will be populated.

export const siteTaskMetadata: RouteMiddleware = async context => {
  const { siteApi } = context.state;
 
  const task = await siteApi.getTask(context.params.taskId);
 
  // Do we need to update the metadata.
  if (siteApi.tasks.requiresUpdate(task)) {
    const newMetadata = await siteApi.tasks.remoteMetadata(task, false);
    const updatedTask = await siteApi.tasks.updateTaskMetadata(task.id, newMetadata);
 
    context.response.body = updatedTask.metadata;
    context.response.status = 200;
    return;
  }
 
  context.response.body = task.metadata;
  context.response.status = 200;
};

The remoteMetadata is what actually calling the resolver class we looked in the beginning, which remember is returning a promise that returns a task (Task: T), then the key is used here to specify exactly what we get back.

  async remoteMetadata<T extends BaseTask>(task: T, fresh = false) {
    const metadata: any = task.metadata || {};
    for (const resolver of this.resolvers) {
      if (fresh || !resolver.hasMetadata(task)) {
        const value = await resolver.resolve(task);
        metadata[resolver.getKey()] = value || null;
      }
    }
    return metadata;
  }

If the task is fetched again using another endpoint like api.getTask() since the metadata was previously resolved it will return with task.metadata without having to make another api call.