ontola/rdfdev-js

View on GitHub
packages/actions/src/iri.ts

Summary

Maintainability
A
0 mins
Test Coverage
import rdf, {
  isBlankNode,
  isTerm,
  NamedNode,
  Namespace, Node,
  PlainFactory,
  SomeTerm,
} from '@ontologies/core';

import {
  ActionExecutor,
  BoundActionDispatcher,
  IRIParams,
  ParsedAction,
} from "./types";

const factory = new PlainFactory();

/**
 * Generate a path?query combination for the given {action} and {payload}.
 *
 * @see {createActionIRI}
 * @see {createActionPair}
 *
 * @param action The path of the action.
 * @param payload
 */
export function actionIRI<ParamMap extends IRIParams<ParamMap> = {}>(
  action: string,
  payload?: Partial<ParamMap>,
): string {

  const query = Object
    .entries<string | SomeTerm>(payload)
    .map<[string, string]>(([k, v]) => [
      k,
      encodeURIComponent(isTerm(v) ? factory.toNQ(v) : v),
    ]);

  return `${action}?${new URLSearchParams(query).toString()}`;
}

/**
 * Create an action generator to easy in IRI generation.
 *
 * @see {actionIRI}
 * @see {createActionPair}
 * @param base - The base Namespace to which the action/payload is added.
 */
export const createActionIRI = <ParamMap extends IRIParams<ParamMap> = {}>(base: Namespace) =>
  (action: string, payload?: Partial<ParamMap>) =>
    base(actionIRI<ParamMap>(action, payload));

/**
 * Create a action creator with the store and namespace already bound.
 *
 * @param base - The namespace to which the action name should be appended.
 * @param store - The store where the action should be dispatched.
 *
 * @see {createActionPair}
 *
 * @example
 *   const dispatch = createActionIRI(store, myNamespace);
 *   store.actions.example = {
 *     initialize: (iri: NamedNode) => dispatch('initialize', { iri })
 *   };
 */
export const createActionNS = <
  ParamMap extends IRIParams<ParamMap> = {},
  Store extends ActionExecutor = any
  >(base: Namespace, store: Store):
  BoundActionDispatcher<ParamMap> => {
  const actionNS = createActionIRI<ParamMap>(base);

  return (action, payload?: Partial<ParamMap>) => store.exec(actionNS(action, payload));
};

/**
 * Create a dispatch-parse pair for use in middleware.
 *
 * @param base - The namespace to create actions on.
 * @param store - The store to dispatch actions on.
 */
export const createActionPair = <
  ParamMap extends IRIParams<ParamMap>,
  Store extends ActionExecutor = any
  >(base: Namespace, store: Store) => ({
  dispatch: createActionNS<ParamMap>(base, store),
  parse: (action: Node) => parseAction<ParamMap>(action),
});

/**
 * Parse an action for its base IRI and parameters separated.
 *
 * Only the last value of each param is included, so duplicates are omitted.
 *
 * The param values are parsed and converted for n-quads syntax, otherwise their literal value is
 * used. The resulting {return.params} is not guaranteed to be correct with the given {ParamMap}, it
 * is for typing purposes only.
 */
export const parseAction = <ParamMap extends IRIParams<ParamMap> = {}>(action: Node):
  ParsedAction<ParamMap> => {


  if (isBlankNode(action)) {
    return {
      action,
      base: action,
      params: {},
    };
  }

  const url = new URL(action.value);
  const params = {};
  url.searchParams.forEach((value: string, key: string) => {
    const t = decodeURIComponent(value);
    try {
      const parsedValue = rdf.termFromNQ(t);
      params[key] = parsedValue ?? t;
    } catch(e) {
      params[key] = t;
    }
  });

  url.search = "";

  return {
    action,
    base: rdf.namedNode(url.toString()),
    params: params as Partial<ParamMap>,
  };
};