scrivito
Version:
Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.
408 lines (340 loc) • 11.8 kB
text/typescript
import difference from 'lodash-es/difference';
import isDate from 'lodash-es/isDate';
import isEmpty from 'lodash-es/isEmpty';
import { AttributeJson, LinkJson } from 'scrivito_sdk/client';
import {
ArgumentError,
formatDateToString,
isCamelCase,
isEmptyValue,
isObject,
isStringArray,
isSystemAttribute,
isValidDateString,
isValidFloat,
isValidInteger,
prettyPrint,
underscore,
} from 'scrivito_sdk/common';
import { ObjJsonPatch } from 'scrivito_sdk/data';
import { NormalizedBasicAttributesWithUnknownValues } from 'scrivito_sdk/models/basic_attribute_content';
import { CmsAttributeType } from 'scrivito_sdk/models/basic_attribute_types';
import { BasicLink } from 'scrivito_sdk/models/basic_link';
import { BasicObj } from 'scrivito_sdk/models/basic_obj';
import { BasicWidget } from 'scrivito_sdk/models/basic_widget';
import { Binary } from 'scrivito_sdk/models/binary';
import { DataLocator } from 'scrivito_sdk/models/data_locator';
import { ObjUnavailable } from 'scrivito_sdk/models/obj_unavailable';
import { BasicTypeInfo } from 'scrivito_sdk/models/type_info';
type ValidLinkInputValue = BasicLink | PlainLinkObject;
interface PlainLinkObject {
obj_id?: string | null;
query?: string | null;
rel?: string | null;
target?: string | null;
title?: string | null;
url?: string | null;
hash?: string | null; // corresponds to the key "fragment" of LinkJson
}
export function serialize(
attributes: NormalizedBasicAttributesWithUnknownValues
): ObjJsonPatch {
const serializedAttributes: ObjJsonPatch = {};
Object.keys(attributes).forEach((name) => {
const serializedName = convertCamelCasedAttributeName(name);
if (isSystemAttribute(serializedName)) {
const [value] = attributes[name] as [ObjJsonPatch[keyof ObjJsonPatch]];
serializedAttributes[serializedName] = value;
} else {
const [value, typeInfo] = attributes[name];
serializedAttributes[serializedName] = serializeAttributeEntry(
value,
name,
typeInfo!
);
}
});
return serializedAttributes;
}
function serializeAttributeEntry<Type extends CmsAttributeType>(
value: unknown,
name: string,
typeInfo: BasicTypeInfo<Type>
): ObjJsonPatch[keyof ObjJsonPatch] {
if (value === null) return null;
const serializedEntry = serializeEntry(value, name, typeInfo);
if (isEmptyValue(serializedEntry[1])) return null;
return serializedEntry;
}
function serializeEntry<Type extends CmsAttributeType>(
value: unknown,
name: string,
typeInfo: BasicTypeInfo<Type>
): AttributeJson {
switch (typeInfo[0]) {
case 'binary':
return ['binary', serializeBinaryAttributeValue(value, name)];
case 'boolean':
return ['boolean', serializeBooleanAttributeValue(value, name)];
case 'datalocator':
return ['datalocator', serializeDataLocatorAttributeValue(value, name)];
case 'date':
return ['date', serializeDateAttributeValue(value, name)];
case 'datetime':
return ['date', serializeDateAttributeValue(value, name)];
case 'enum':
return ['string', serializeEnumAttributeValue(value, name, typeInfo[1])];
case 'float':
return ['number', serializeFloatAttributeValue(value, name)];
case 'html':
return ['html', serializeHtmlAttributeValue(value, name)];
case 'integer':
return ['number', serializeIntegerAttributeValue(value, name)];
case 'link':
return ['link', serializeLinkAttributeValue(value, name)];
case 'linklist':
return ['linklist', serializeLinklistAttributeValue(value, name)];
case 'multienum':
return [
'stringlist',
serializeMultienumAttributeValue(value, name, typeInfo[1]),
];
case 'reference':
return ['reference', serializeReferenceAttributeValue(value, name)];
case 'referencelist':
return [
'referencelist',
serializeReferencelistAttributeValue(value, name),
];
case 'string':
return ['string', serializeStringAttributeValue(value, name)];
case 'stringlist':
return ['stringlist', serializeStringlistAttributeValue(value, name)];
case 'widget':
return ['widget', serializeWidgetAttributeValue(value, name)];
case 'widgetlist':
return ['widgetlist', serializeWidgetlistAttributeValue(value, name)];
default:
throw new ArgumentError(
`Attribute "${name}" is of unsupported type "${typeInfo[0]}".`
);
}
}
function throwInvalidAttributeValue(
value: unknown,
name: string,
expected: string
): never {
throw new ArgumentError(
`Unexpected value ${prettyPrint(value)} for` +
` attribute "${name}". Expected: ${expected}`
);
}
function serializeBinaryAttributeValue(value: unknown, name: string) {
if (value instanceof Binary) return { id: value.id() };
throwInvalidAttributeValue(value, name, 'A Binary.');
}
function serializeBooleanAttributeValue(value: unknown, name: string) {
if (value === false || value === true) return value;
throwInvalidAttributeValue(value, name, 'A Boolean.');
}
function serializeDataLocatorAttributeValue(value: unknown, name: string) {
if (value instanceof DataLocator) return value.toPojo();
throwInvalidAttributeValue(value, name, 'A DataLocator.');
}
function serializeDateAttributeValue(value: unknown, name: string) {
if (isDate(value)) return formatDateToString(value);
if (isValidDateString(value)) return value;
throwInvalidAttributeValue(value, name, 'A Date.');
}
function serializeEnumAttributeValue(
value: unknown,
name: string,
{ values }: { values: readonly string[] }
) {
if (values.includes(value as string)) return value as string;
const e = `Valid attribute values are contained in its "values" array [${values.join()}].`;
throwInvalidAttributeValue(value, name, e);
}
function serializeFloatAttributeValue(value: unknown, name: string) {
if (isValidFloat(value)) return value;
let invalidValue = value;
if (typeof value === 'number') {
invalidValue = String(value);
}
throwInvalidAttributeValue(
invalidValue,
name,
'A Number, that is #isFinite().'
);
}
function serializeHtmlAttributeValue(value: unknown, name: string) {
if (typeof value === 'string') return value;
throwInvalidAttributeValue(value, name, 'A String.');
}
function serializeIntegerAttributeValue(value: unknown, name: string) {
if (isValidInteger(value)) return value;
throwInvalidAttributeValue(
value,
name,
'A Number, that is #isSafeInteger().'
);
}
function serializeLinkAttributeValue(value: unknown, name: string) {
if (isValidLinkInputValue(value)) return convertLinkToCmsApi(value);
throwInvalidAttributeValue(
value,
name,
'A Link instance with a destination.'
);
}
function serializeLinklistAttributeValue(value: unknown, name: string) {
if (Array.isArray(value) && value.every(isValidLinkInputValue)) {
return value.map(convertLinkToCmsApi);
}
throwInvalidAttributeValue(
value,
name,
'An array of Link instances with destinations set.'
);
}
function serializeMultienumAttributeValue(
value: unknown,
name: string,
{ values }: { values: readonly string[] }
) {
if (!isStringArray(value)) {
throwInvalidAttributeValue(
value,
name,
`An array with values from ${prettyPrint(values)}.`
);
}
const forbiddenValues = difference(value, values);
if (forbiddenValues.length) {
throwInvalidAttributeValue(
value,
name,
`An array with values from ${prettyPrint(
values
)}. Forbidden values: ${prettyPrint(forbiddenValues)}.`
);
}
return value;
}
function serializeReferenceAttributeValue(value: unknown, name: string) {
if (isValidReference(value)) return serializeSingleReferenceValue(value);
throwInvalidAttributeValue(value, name, 'An Obj.');
}
function serializeReferencelistAttributeValue(value: unknown, name: string) {
if (isValidReferencelistValue(value)) {
return value.map(serializeSingleReferenceValue);
}
throwInvalidAttributeValue(value, name, 'An array with Objs.');
}
function serializeSingleReferenceValue(
value: string | BasicObj | ObjUnavailable
) {
return typeof value === 'string' ? value : value.id();
}
function isValidReference(
value: unknown
): value is string | BasicObj | ObjUnavailable {
return (
typeof value === 'string' ||
value instanceof BasicObj ||
value instanceof ObjUnavailable
);
}
function isValidReferencelistValue(
value: unknown
): value is (string | BasicObj | ObjUnavailable)[] {
return Array.isArray(value) && value.every((v) => isValidReference(v));
}
function serializeStringAttributeValue(value: unknown, name: string) {
if (isValidString(value)) return value.toString();
throwInvalidAttributeValue(value, name, 'A String.');
}
function serializeStringlistAttributeValue(value: unknown, name: string) {
if (isStringOrNumberArray(value)) {
return value.map((v) => v.toString());
}
throwInvalidAttributeValue(value, name, 'An array of strings.');
}
function isValidString(value: unknown): value is string | number {
return typeof value === 'string' || typeof value === 'number';
}
function serializeWidgetAttributeValue(value: unknown, name: string): string {
if (value instanceof BasicWidget) return value.id();
throwInvalidAttributeValue(value, name, 'An instance of Widget.');
}
function serializeWidgetlistAttributeValue(
value: unknown,
name: string
): string[] {
if (value instanceof BasicWidget) {
return serializeWidgetlistAttributeValue([value], name);
}
if (isBasicWidgetArray(value)) return value.map((v) => v.id());
throwInvalidAttributeValue(value, name, 'An array of Widget instances.');
}
function isBasicWidgetArray(value: unknown): value is BasicWidget[] {
return Array.isArray(value) && value.every((v) => v instanceof BasicWidget);
}
function isStringOrNumberArray(
value: unknown
): value is Array<string | number> {
return Array.isArray(value) && value.every((v) => isValidString(v));
}
function isValidLinkInputValue(value: unknown): value is ValidLinkInputValue {
// check if value is backend compatible
if (value instanceof BasicLink) return !value.isEmpty();
if (!isObject(value)) return false;
if (isEmpty(Object.values(value).filter(Boolean))) return false;
const invalidKeys = difference(
Object.keys(value as { [key: string]: unknown }),
['hash', 'obj_id', 'query', 'rel', 'target', 'title', 'url']
);
return isEmpty(invalidKeys);
}
function convertCamelCasedAttributeName(name: string) {
if (!isCamelCase(name)) {
throw new ArgumentError('Attribute names have to be in camel case.');
}
return underscore(name);
}
function convertLinkToCmsApi(value: ValidLinkInputValue): LinkJson {
const cmsApiValue =
value instanceof BasicLink
? convertBasicLinkToCmsApi(value)
: convertLinkObjectToCmsApi(value);
if (!cmsApiValue.rel) {
delete cmsApiValue.rel;
}
return cmsApiValue;
}
function convertBasicLinkToCmsApi(basicLink: BasicLink): LinkJson {
return {
rel: basicLink.rel() || undefined,
query: basicLink.query(),
target: basicLink.target(),
title: basicLink.title(),
url: basicLink.url(),
// lowercased property method
obj_id: basicLink.objId(),
// different property method
fragment: basicLink.hash(),
};
}
function convertLinkObjectToCmsApi(value: Readonly<PlainLinkObject>): LinkJson {
return {
obj_id: value.obj_id || null,
query: value.query || null,
rel: value.rel || undefined,
target: value.target || null,
title: value.title || null,
url: value.url || null,
// different property key
fragment: value.hash || null,
};
}