@appsemble/lang-sdk
Version:
Language SDK for Appsemble
697 lines • 27.2 kB
JavaScript
import { filter, literalValues, param } from '@odata/parser';
import { addMilliseconds, format, parse, parseISO } from 'date-fns';
import equal from 'fast-deep-equal';
import { XMLParser } from 'fast-xml-parser';
import { createEvent } from 'ics';
import parseDuration from 'parse-duration';
import { getDuration, processLocation } from './ics.js';
import { mapValues } from './mapValues.js';
import { has, stripNullValues } from './miscellaneous.js';
class RemapperError extends TypeError {
constructor(message, remapper) {
super(message);
this.name = 'RemapperError';
this.remapper = remapper;
}
}
function isPlainObject(value) {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function isEqualArray(a, b) {
if (a.length !== b.length) {
return false;
}
return a.every((val, i) => val === b[i]);
}
function isEqualObject(a, b) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
return aKeys.every((key) => bKeys.includes(key) && equal(a[key], b[key]));
}
function isNumber(a) {
return a !== undefined && a != null && a !== '' && !Number.isNaN(Number(a));
}
export function remap(remapper, input, context) {
if (typeof remapper === 'string' ||
typeof remapper === 'number' ||
typeof remapper === 'boolean' ||
remapper == null) {
return remapper;
}
let result = input;
// Workaround for ts(2589) Type instantiation is excessively deep and possibly infinite
const remappers = Array.isArray(remapper)
? remapper.flat(Number.POSITIVE_INFINITY)
: [remapper];
for (const mapper of remappers) {
const entries = Object.entries(mapper);
if (entries.length !== 1) {
console.error(mapper);
throw new RemapperError(`Remapper has multiple keys: ${Object.keys(mapper)
.map((key) => JSON.stringify(key))
.join(', ')}`, mapper);
}
const [[name, args]] = entries;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (!has(mapperImplementations, name)) {
console.error(mapper);
throw new RemapperError(`Remapper name does not exist: ${JSON.stringify(name)}`, mapper);
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const implementation = mapperImplementations[name];
result = implementation(args, result, { root: input, ...context });
}
return result;
}
/**
* Implementations of all remappers.
*
* All arguments are deferred from remappers.
*
* @see Remappers
*/
const mapperImplementations = {
app(prop, input, context) {
if (prop === 'id') {
return context.appId;
}
if (prop === 'locale') {
return context.locale;
}
if (prop === 'url') {
return context.appUrl;
}
throw new Error(`Unknown app property: ${prop}`);
},
page(prop, input, context) {
if (prop === 'data') {
return context.pageData;
}
if (prop === 'url') {
return context.url;
}
throw new Error(`Unknown page property: ${prop}`);
},
context(prop, input, context) {
let result = context.context;
for (const p of String(prop).split('.')) {
if (result == null) {
return null;
}
result = result[p];
}
return result ?? null;
},
variable(name, input, context) {
if (context.getVariable) {
return context.getVariable(name);
}
return { variable: name };
},
equals(mappers, input, context) {
if (mappers.length <= 1) {
return true;
}
const values = mappers.map((mapper) => remap(mapper, input, context));
return values.every((value) => equal(values[0], value));
},
not(mappers, input, context) {
if (mappers.length <= 1) {
return !remap(mappers[0], input, context);
}
const [firstValue, ...otherValues] = mappers.map((mapper) => remap(mapper, input, context));
return !otherValues.some((value) => equal(firstValue, value));
},
or(mappers, input, context) {
const values = mappers.map((mapper) => Boolean(remap(mapper, input, context)));
return values.length > 0 ? values.includes(true) : true;
},
and(mappers, input, context) {
const values = mappers.map((mapper) => remap(mapper, input, context));
return values.every(Boolean);
},
step(mapper, input, context) {
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 18048 variable is possibly undefined (strictNullChecks)
return context.stepRef.current[mapper];
},
'tab.name'(mapper, input, context) {
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 18048 variable is possibly undefined (strictNullChecks)
return context.tabRef.current.name;
},
gt: ([left, right], input, context) =>
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore Messed up - 2571 Object is of type 'unknown'.
remap(left, input, context) > remap(right, input, context),
lt: ([left, right], input, context) =>
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore Messed up - 2571 Object is of type 'unknown'.
remap(left, input, context) < remap(right, input, context),
ics(mappers, input, context) {
let event;
const mappedStart = remap(mappers.start, input, context);
const start = mappedStart instanceof Date ? mappedStart : parseISO(mappedStart);
const sharedAttributes = {
start: [
start.getUTCFullYear(),
start.getUTCMonth() + 1,
start.getUTCDate(),
start.getUTCHours(),
start.getUTCMinutes(),
],
startInputType: 'utc',
startOutputType: 'utc',
title: remap(mappers.title, input, context),
description: remap(mappers.description ?? null, input, context),
url: remap(mappers.url ?? null, input, context),
location: remap(mappers.location ?? null, input, context),
geo: processLocation(remap(mappers.coordinates ?? null, input, context)),
productId: context.appUrl,
};
if ('end' in mappers) {
const mappedEnd = remap(mappers.end, input, context);
const end = mappedEnd instanceof Date ? mappedEnd : parseISO(mappedEnd);
event = {
...sharedAttributes,
endInputType: 'utc',
endOutputType: 'utc',
end: [
end.getUTCFullYear(),
end.getUTCMonth() + 1,
end.getUTCDate(),
end.getUTCHours(),
end.getUTCMinutes(),
],
};
}
else {
event = {
...sharedAttributes,
duration: getDuration(remap(mappers.duration, input, context)),
};
}
const { error, value } = createEvent(event);
if (error) {
throw error;
}
return value;
},
if(mappers, input, context) {
const condition = remap(mappers.condition, input, context);
return remap(condition ? mappers.then : mappers.else, input, context);
},
match(mappers, input, context) {
return (remap(mappers.find((mapper) => remap(mapper.case, input, context))?.value ?? null, input, context) ?? null);
},
'object.from': (mappers, input, context) => mapValues(mappers, (mapper) => remap(mapper, input, context)),
'object.assign': (mappers, input, context) => ({
...input,
...mapValues(mappers, (mapper) => remap(mapper, input, context)),
}),
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'object.omit'(keys, input) {
const result = { ...input };
for (const key of keys) {
if (Array.isArray(key)) {
let acc = result;
for (const [index, k] of key.entries()) {
if (index === key.length - 1) {
delete acc[k];
}
else {
acc = acc?.[k];
}
}
}
else {
delete result[key];
}
}
return result;
},
'object.compare'([remapper1, remapper2], input, context) {
const remapped1 = remap(remapper1, input, context);
const remapped2 = remap(remapper2, input, context);
function deepDiff(object1, object2) {
const stack = [{ obj1: object1, obj2: object2, path: [] }];
const diffs = [];
while (stack.length !== 0) {
const { obj1, obj2, path } = stack.pop();
const keys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
for (const key of keys) {
const val1 = obj1?.[key];
const val2 = obj2?.[key];
const currentPath = [...path, key];
if (isPlainObject(val1) && isPlainObject(val2)) {
stack.push({ obj1: val1, obj2: val2, path: currentPath });
}
else if (Array.isArray(val1) && Array.isArray(val2) && !isEqualArray(val1, val2)) {
diffs.push({ path: currentPath, type: 'changed', from: val1, to: val2 });
}
else if (!(key in obj1)) {
diffs.push({ path: currentPath, type: 'added', value: val2 });
}
else if (!(key in obj2)) {
diffs.push({ path: currentPath, type: 'removed', value: val1 });
}
else if (val1 !== val2) {
diffs.push({ path: currentPath, type: 'changed', from: val1, to: val2 });
}
}
}
return diffs;
}
if (!isPlainObject(remapped1) || !isPlainObject(remapped2)) {
return [];
}
return deepDiff(remapped1, remapped2);
},
'object.explode'(property, input) {
if (!isPlainObject(input)) {
return [];
}
const { [property]: arrayValue, ...rest } = input;
if (!Array.isArray(arrayValue)) {
return [];
}
return arrayValue.map((item) => ({
...rest,
...item,
}));
},
type(args, input) {
// eslint-disable-next-line eqeqeq
if (input === null) {
return null;
}
if (Array.isArray(input)) {
return 'array';
}
return typeof input;
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'array.map': (mapper, input, context) => input?.map((item, index) => remap(mapper, item, {
...context,
array: {
index,
length: input.length,
item,
prevItem: input[index - 1],
nextItem: input[index + 1],
},
})) ?? [],
'array.contains'(mapper, input, context) {
if (!Array.isArray(input)) {
return false;
}
const remapped = remap(mapper, input, context);
if (isPlainObject(remapped)) {
return input.some((item) => isEqualObject(item, remapped ?? {}));
}
if (Array.isArray(remapped)) {
return input.some((item) => isEqualArray(item, remapped));
}
return input.includes(remapped);
},
'array.join'(separator, input) {
if (!Array.isArray(input)) {
return input;
}
return input.join(separator ?? undefined);
},
'array.unique'(mapper, input, context) {
if (!Array.isArray(input)) {
return input;
}
const remapped = input.map((item, index) => mapper == null
? item
: remap(mapper, item, {
...context,
array: {
index,
length: input.length,
item,
prevItem: input[index - 1],
nextItem: input[index + 1],
},
}));
return input.filter((value, index) => {
for (let i = 0; i < index; i += 1) {
if (equal(remapped[index], remapped[i])) {
return false;
}
}
return true;
});
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
array: (prop, input, context) => context.array?.[prop],
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'array.filter'(mapper, input, context) {
if (!Array.isArray(input)) {
console.error(`${input} is not an array!`);
return null;
}
return input?.filter((item, index) => {
const remapped = remap(mapper, item, {
...context,
array: {
index,
length: input.length,
item,
prevItem: input[index - 1],
nextItem: input[index + 1],
},
});
return remapped;
});
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'array.find'(mapper, input, context) {
if (!Array.isArray(input)) {
console.error(`${input} is not an array!`);
return null;
}
return (input?.find((item) => {
const remapped = remap(mapper, item, context);
switch (typeof remapped) {
case 'boolean':
return remap(mapper, item, context) ? item : null;
default:
return equal(remapped, item) ? item : null;
}
}) ?? null);
},
'array.from': (mappers, input, context) => mappers.map((mapper) => remap(mapper, input, context)),
'array.append': (mappers, input, context) => Array.isArray(input)
? input.concat(mappers.map((mapper) => remap(mapper, input, context)))
: [],
'array.omit'(mappers, input, context) {
const indices = new Set(mappers.map((mapper) => {
const remapped = remap(mapper, input, context);
if (typeof remapped === 'number') {
return remapped;
}
}));
return Array.isArray(input) ? input.filter((value, i) => !indices.has(i)) : [];
},
'array.flatten'(mapper, input, context) {
if (!Array.isArray(input)) {
return input;
}
const depth = remap(mapper, input, context);
return input.flat(depth || Number.POSITIVE_INFINITY);
},
static: (input) => input,
prop(prop, obj, context) {
let result = obj;
if (result == null) {
return result;
}
if (Array.isArray(prop)) {
if (prop.every((item) => typeof item === 'number' || typeof item === 'string')) {
// This runs if the provided value is an array of property names or indexes
for (const p of [prop].flat()) {
result = result[p];
}
}
else if (prop.every((item) => typeof item === 'object' && !Array.isArray(item))) {
// This runs if the provided value is an array of remappers
const remapped = remap(prop, obj, context);
if (typeof remapped === 'number' || typeof remapped === 'string') {
result = result[remapped];
}
else {
console.error(`Invalid remapper ${JSON.stringify(prop)}`);
}
}
}
else if (typeof prop === 'object') {
if (prop == null) {
result = result.null;
return result;
}
// This runs if the provided value is a remapper
const remapped = remap(prop, result, context);
if (typeof remapped === 'number' || typeof remapped === 'string') {
result = result[remapped];
}
else {
console.error(`Invalid remapper ${JSON.stringify(prop)}`);
}
}
else if (typeof prop === 'number' || typeof prop === 'string') {
result =
Array.isArray(result) && typeof prop === 'number' && prop < 0
? result[result.length + prop]
: result[prop];
}
return result;
},
'number.parse'(remapper, input, context) {
if (!remapper) {
const inputConverted = Number(input);
if (!Number.isNaN(inputConverted)) {
return inputConverted;
}
return input;
}
const remapped = remap(remapper, input, context);
const remappedConverted = Number(remapped);
if (!Number.isNaN(remappedConverted)) {
return remappedConverted;
}
return remapped;
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'date.parse': (fmt, input) => (fmt ? parse(input, fmt, new Date()) : parseISO(input)),
'date.now': () => new Date(),
'date.add'(time, input) {
const expireDuration = parseDuration(time);
if (!expireDuration || !input || (!Number.isFinite(input) && !(input instanceof Date))) {
return input;
}
return addMilliseconds(input, expireDuration);
},
'date.format'(args, input) {
const date = input instanceof Date
? input
: typeof input === 'number'
? new Date(input)
: parseISO(String(input));
return args ? format(date, args) : date.toJSON();
},
'null.strip': (args, input) => stripNullValues(input, args || {}),
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'random.choice': (args, input) => Array.isArray(input) ? input[Math.floor(Math.random() * input.length)] : input,
'random.integer'(args) {
const min = Math.min(...args);
const max = Math.max(...args);
return Math.floor(Math.random() * (max - min) + min);
},
'random.float'(args) {
const min = Math.min(...args);
const max = Math.max(...args);
return Math.random() * (max - min) + min;
},
'random.string'(args) {
const result = [];
const characters = [...new Set(args.choice.split(''))];
for (let i = 0; i <= args.length; i += 1) {
result.push(characters[Math.floor(Math.random() * characters.length)]);
}
return result.join('');
},
root: (args, input, context) => context.root,
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
len: (args, input) => input?.length,
history: (index, input, context) => context.history?.[index],
'from.history': ({ index, props }, input, context) => mapValues(props, (mapper) => remap(mapper, context.history?.[index], context)),
'assign.history': ({ index, props }, input, context) => ({
...input,
...mapValues(props, (mapper) => remap(mapper, context.history?.[index], context)),
}),
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'omit.history'({ index, keys }, input, context) {
const result = { ...context.history?.[index] };
for (const key of keys) {
if (Array.isArray(key)) {
let acc = result;
for (const [i, k] of key.entries()) {
if (i === key.length - 1) {
delete acc[k];
}
else {
acc = acc?.[k];
}
}
}
else {
delete result[key];
}
}
return { ...input, ...result };
},
'string.case'(stringCase, input) {
if (stringCase === 'lower') {
return String(input).toLowerCase();
}
if (stringCase === 'upper') {
return String(input).toUpperCase();
}
return input;
},
'string.contains'(stringToCheck, input) {
if (!(typeof input === 'string')) {
return false;
}
return input.includes(stringToCheck);
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'string.startsWith'(substring, input) {
if (typeof substring === 'string') {
return input.startsWith(substring);
}
if (substring.strict || substring.strict === undefined) {
return input.startsWith(substring.substring);
}
return input.toLowerCase().startsWith(substring.substring.toLowerCase());
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 ... is not assignable to type (strictNullChecks)
'string.endsWith'(substring, input) {
if (typeof substring === 'string') {
return input.endsWith(substring);
}
if (substring.strict || substring.strict === undefined) {
return input.endsWith(substring.substring);
}
return input.toLowerCase().endsWith(substring.substring.toLowerCase());
},
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore 2322 null is not assignable to type (strictNullChecks)
slice(sliceIndex, input) {
try {
return Array.isArray(sliceIndex) ? input.slice(...sliceIndex) : input.slice(sliceIndex);
}
catch {
return null;
}
},
log(level, input, context) {
console[level ?? 'info'](JSON.stringify({ input, context }, null, 2));
return input;
},
'string.format'({ messageId, template, values }, input, context) {
try {
const remappedMessageId = remap(messageId ?? null, input, context);
const message = context.getMessage({ id: remappedMessageId, defaultMessage: template });
return message.format(values ? mapValues(values, (val) => remap(val, input, context)) : undefined);
}
catch (error) {
if (messageId) {
return `{${messageId}}`;
}
return error.message;
}
},
'string.replace'(values, input) {
const [[regex, replacer]] = Object.entries(values);
return String(input).replaceAll(new RegExp(regex, 'gm'), replacer);
},
translate(messageId, input, context) {
const remappedId = remap(messageId, input, context);
if (typeof remappedId !== 'string') {
return null;
}
const message = context.getMessage({ id: remappedId });
return message.format() || `{${remappedId}}`;
},
'app.member': (property, input, context) => context.appMemberInfo?.[property],
container(property, input, context) {
// This value is replaced when the request is made
// By using the value of the release name
const namespace = 'companion-containers-appsemble';
const appName = context.appUrl.split('.')[0].replace(/^https?:\/\//, '');
const endpoint = property.split('/').slice(1).join('/');
const containerName = `${property.split('/')[0]}-${appName}-${context.appId}`
.replaceAll(' ', '-')
.toLowerCase();
const url = `http://${containerName}.${namespace}.svc.cluster.local/${endpoint}`;
return url;
},
'filter.from'(values, input, context) {
let result = filter();
for (const [field, { comparator, type, value }] of Object.entries(values)) {
const remapped = remap(value, input, context);
const remappedDefined = remapped === undefined ? null : remapped;
const literal = type === 'String' && remapped != null
? literalValues[type](String(remappedDefined).replaceAll("'", "''").replaceAll('\\', '\\\\'))
: literalValues[type === 'Number' ? 'Decimal' : type](remappedDefined);
result = result.field(field)[comparator](literal);
}
return String(param().filter(result)).replace(/^\$filter=/, '');
},
'order.from'(values) {
return String(param().orderby(Object.entries(values).map(([key, order]) => ({ field: key, order })))).replace('$orderby=', '');
},
'xml.parse'(value, input, context) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
});
try {
return parser.parse(remap(value, input, context) || '');
}
catch (error) {
console.error(error);
return {};
}
},
defined(value, input, context) {
const remapped = remap(value, input, context);
return remapped !== undefined && remapped != null;
},
maths(value, input, context) {
const { a, b, operation } = value;
const aRemapped = remap(a, input, context);
const bRemapped = remap(b, input, context);
if (!isNumber(aRemapped) || !isNumber(bRemapped)) {
return -1;
}
const na = Number(aRemapped);
const nb = Number(bRemapped);
switch (operation) {
case 'add':
return na + nb;
case 'subtract':
return na - nb;
case 'multiply':
return na * nb;
case 'divide':
if (nb === 0) {
return -1;
}
return na / nb;
case 'mod':
return na % nb;
default:
return -1;
}
},
};
//# sourceMappingURL=remap.js.map