@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
504 lines (467 loc) • 17.5 kB
text/typescript
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import type { Literal, NamedNode } from "@rdfjs/types";
import type { Url, UrlString, Thing, ThingPersisted } from "../interfaces";
import type { XmlSchemaTypeIri, Time } from "../datatypes";
import {
normalizeLocale,
xmlSchemaTypes,
deserializeBoolean,
deserializeDatetime,
deserializeDecimal,
deserializeInteger,
internal_isValidUrl,
deserializeDate,
deserializeTime,
} from "../datatypes";
import { internal_throwIfNotThing } from "./thing.internal";
import {
isThing,
ValidPropertyUrlExpectedError,
ValidValueUrlExpectedError,
asIri,
} from "./thing";
import { internal_toIriString } from "../interfaces.internal";
import { freeze } from "../rdf.internal";
/**
* Create a new Thing with all values removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing [[Thing]] to remove values from.
* @param property Property for which to remove all values from the Thing.
* @returns A new Thing equal to the input Thing with all values removed for the given Property.
*/
export function removeAll<T extends Thing>(
thing: T,
property: Url | UrlString,
): T;
export function removeAll(thing: Thing, property: Url | UrlString): Thing {
internal_throwIfNotThing(thing);
if (!internal_isValidUrl(property)) {
throw new ValidPropertyUrlExpectedError(property);
}
const predicateIri = internal_toIriString(property);
const newPredicates = { ...thing.predicates };
delete newPredicates[predicateIri];
return freeze({
...thing,
predicates: freeze(newPredicates),
});
}
/**
* Create a new Thing with the given URL removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a URL value from.
* @param property Property for which to remove the given URL value.
* @param value URL to remove from `thing` for the given `Property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeUrl: RemoveOfType<Url | UrlString | ThingPersisted> = (
thing,
property,
value,
) => {
internal_throwIfNotThing(thing);
if (!internal_isValidUrl(property)) {
throw new ValidPropertyUrlExpectedError(property);
}
const predicateIri = internal_toIriString(property);
if (!isThing(value) && !internal_isValidUrl(value)) {
throw new ValidValueUrlExpectedError(value);
}
const iriToRemove = isThing(value)
? asIri(value)
: internal_toIriString(value);
const updatedNamedNodes = freeze(
thing.predicates[predicateIri]?.namedNodes?.filter(
(namedNode) => namedNode.toLowerCase() !== iriToRemove.toLowerCase(),
) ?? [],
);
const updatedPredicate = freeze({
...thing.predicates[predicateIri],
namedNodes: updatedNamedNodes,
});
const updatedPredicates = freeze({
...thing.predicates,
[predicateIri]: updatedPredicate,
});
return freeze({
...thing,
predicates: updatedPredicates,
});
};
/** @hidden Alias of [[removeUrl]] for those who prefer IRI terminology. */
export const removeIri = removeUrl;
/**
* Create a new Thing with the given boolean removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a boolean value from.
* @param property Property for which to remove the given boolean value.
* @param value Boolean to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeBoolean: RemoveOfType<boolean> = (
thing,
property,
value,
) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.boolean,
(foundBoolean) => deserializeBoolean(foundBoolean) === value,
);
};
/**
* Create a new Thing with the given datetime removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a datetime value from.
* @param property Property for which to remove the given datetime value.
* @param value Datetime to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeDatetime: RemoveOfType<Date> = (thing, property, value) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.dateTime,
(foundDatetime) =>
deserializeDatetime(foundDatetime)?.getTime() === value.getTime(),
);
};
/**
* Create a new Thing with the given date removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a date value from.
* @param property Property for which to remove the given date value.
* @param value Date to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
* @since 1.10.0
*/
export const removeDate: RemoveOfType<Date> = (thing, property, value) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.date,
function (foundDate) {
const deserializedDate = deserializeDate(foundDate);
if (deserializedDate) {
return (
deserializedDate.getFullYear() === value.getFullYear() &&
deserializedDate.getMonth() === value.getMonth() &&
deserializedDate.getUTCDate() === value.getUTCDate()
);
}
return false;
},
);
};
/**
* Create a new Thing with the given datetime removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a datetime value from.
* @param property Property for which to remove the given datetime value.
* @param value Time to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
* @since 1.10.0
*/
export const removeTime: RemoveOfType<Time> = (thing, property, value) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.time,
function (foundTime) {
const deserializedTime = deserializeTime(foundTime);
if (deserializedTime) {
return (
deserializedTime.hour === value.hour &&
deserializedTime.minute === value.minute &&
deserializedTime.second === value.second &&
deserializedTime.millisecond === value.millisecond &&
deserializedTime.timezoneHourOffset === value.timezoneHourOffset &&
deserializedTime.timezoneMinuteOffset === value.timezoneMinuteOffset
);
}
return false;
},
);
};
/**
* Create a new Thing with the given decimal removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a decimal value from.
* @param property Property for which to remove the given decimal value.
* @param value Decimal to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeDecimal: RemoveOfType<number> = (thing, property, value) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.decimal,
(foundDecimal) => deserializeDecimal(foundDecimal) === value,
);
};
/**
* Create a new Thing with the given integer removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove an integer value from.
* @param property Property for which to remove the given integer value.
* @param value Integer to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeInteger: RemoveOfType<number> = (thing, property, value) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.integer,
(foundInteger) => deserializeInteger(foundInteger) === value,
);
};
/**
* Create a new Thing with the given English string removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a localised string value from.
* @param property Property for which to remove the given localised string value.
* @param value String to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
* @since 1.13.0
*/
export function removeStringEnglish<T extends Thing>(
thing: T,
property: Url | UrlString,
value: string,
): T {
return removeStringWithLocale(thing, property, value, "en");
}
/**
* Create a new Thing with the given localised string removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove a localised string value from.
* @param property Property for which to remove the given localised string value.
* @param value String to remove from `thing` for the given `property`.
* @param locale Locale of the string to remove.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export function removeStringWithLocale<T extends Thing>(
thing: T,
property: Url | UrlString,
value: string,
locale: string,
): T {
internal_throwIfNotThing(thing);
if (!internal_isValidUrl(property)) {
throw new ValidPropertyUrlExpectedError(property);
}
const predicateIri = internal_toIriString(property);
const existingLangStrings = thing.predicates[predicateIri]?.langStrings ?? {};
const matchingLocale = Object.keys(existingLangStrings).find(
(existingLocale) =>
normalizeLocale(existingLocale) === normalizeLocale(locale) &&
Array.isArray(existingLangStrings[existingLocale]) &&
existingLangStrings[existingLocale].length > 0,
);
if (typeof matchingLocale !== "string") {
// Nothing to remove.
return thing;
}
const existingStringsInLocale = existingLangStrings[matchingLocale];
const updatedStringsInLocale = freeze(
existingStringsInLocale.filter(
(existingString) => existingString !== value,
),
);
const updatedLangStrings = freeze({
...existingLangStrings,
[matchingLocale]: updatedStringsInLocale,
});
const updatedPredicate = freeze({
...thing.predicates[predicateIri],
langStrings: updatedLangStrings,
});
const updatedPredicates = freeze({
...thing.predicates,
[predicateIri]: updatedPredicate,
});
return freeze({
...thing,
predicates: updatedPredicates,
});
}
/**
* Create a new Thing with the given unlocalised string removed for the given Property.
*
* The original `thing` is not modified; this function returns a cloned Thing with updated values.
*
* @param thing Thing to remove an unlocalised string value from.
* @param property Property for which to remove the given string value.
* @param value String to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export const removeStringNoLocale: RemoveOfType<string> = (
thing,
property,
value,
) => {
internal_throwIfNotThing(thing);
return removeLiteralMatching(
thing,
property,
xmlSchemaTypes.string,
(foundString) => foundString === value,
);
};
/**
* @ignore This should not be needed due to the other remove*() functions. If you do find yourself needing it, please file a feature request for your use case.
* @param thing Thing to remove a NamedNode value from.
* @param property Property for which to remove the given NamedNode value.
* @param value NamedNode to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export function removeNamedNode<T extends Thing>(
thing: T,
property: Url | UrlString,
value: NamedNode,
): T {
return removeUrl(thing, property, value.value);
}
/**
* @ignore This should not be needed due to the other remove*() functions. If you do find yourself needing it, please file a feature request for your use case.
* @param thing Thing to remove a Literal value from.
* @param property Property for which to remove the given Literal value.
* @param value Literal to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export function removeLiteral<T extends Thing>(
thing: T,
property: Url | UrlString,
value: Literal,
): T {
internal_throwIfNotThing(thing);
if (!internal_isValidUrl(property)) {
throw new ValidPropertyUrlExpectedError(property);
}
const typeIri = value.datatype.value;
if (typeIri === xmlSchemaTypes.langString) {
return removeStringWithLocale(thing, property, value.value, value.language);
}
const predicateIri = internal_toIriString(property);
const existingPredicateValues = thing.predicates[predicateIri] ?? {};
const existingLiterals = existingPredicateValues.literals ?? {};
const existingValuesOfType = existingLiterals[typeIri] ?? [];
const updatedValues = freeze(
existingValuesOfType.filter(
(existingValue) => existingValue !== value.value,
),
);
const updatedLiterals = freeze({
...existingLiterals,
[typeIri]: updatedValues,
});
const updatedPredicate = freeze({
...existingPredicateValues,
literals: updatedLiterals,
});
const updatedPredicates = freeze({
...thing.predicates,
[predicateIri]: updatedPredicate,
});
const updatedThing = freeze({
...thing,
predicates: updatedPredicates,
});
return updatedThing;
}
/**
* @param thing Thing to remove a Literal value from.
* @param property Property for which to remove the given Literal value.
* @param type Data type that the Literal should be stored as.
* @param matcher Function that returns true if the given value is an equivalent serialisation of the value to remove. For example, when removing a `false` boolean, the matcher should return true for both "0" and "false".
*/
function removeLiteralMatching<T extends Thing>(
thing: T,
property: Url | UrlString,
type: XmlSchemaTypeIri,
matcher: (serialisedValue: string) => boolean,
): T {
if (!internal_isValidUrl(property)) {
throw new ValidPropertyUrlExpectedError(property);
}
const predicateIri = internal_toIriString(property);
const existingPredicateValues = thing.predicates[predicateIri] ?? {};
const existingLiterals = existingPredicateValues.literals ?? {};
const existingValuesOfType = existingLiterals[type] ?? [];
const updatedValues = freeze(
existingValuesOfType.filter((existingValue) => !matcher(existingValue)),
);
const updatedLiterals = freeze({
...existingLiterals,
[type]: updatedValues,
});
const updatedPredicate = freeze({
...existingPredicateValues,
literals: updatedLiterals,
});
const updatedPredicates = freeze({
...thing.predicates,
[predicateIri]: updatedPredicate,
});
const updatedThing = freeze({
...thing,
predicates: updatedPredicates,
});
return updatedThing;
}
/**
* @param thing Thing to remove a value from.
* @param property Property for which to remove the given value.
* @param value Value to remove from `thing` for the given `property`.
* @returns A new Thing equal to the input Thing with the given value removed for the given Property.
*/
export type RemoveOfType<Type> = <T extends Thing>(
thing: T,
property: Url | UrlString,
value: Type,
) => T;