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 ProjectTaskMetadatasubject-resolver
- that returns a snippet of data for a canvas, manifest or collectionselector-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.