"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseChannelType = exports.DEFAULT_SUBSCRIPTION_SHACL = exports.DEFAULT_NOTIFICATION_FEATURES = void 0;
const stream_1 = require("stream");
const context_entries_1 = require("@comunica/context-entries");
const iso8601_duration_1 = require("iso8601-duration");
const n3_1 = require("n3");
const rdf_parse_1 = __importDefault(require("rdf-parse"));
const rdf_validate_shacl_1 = __importDefault(require("rdf-validate-shacl"));
const uuid_1 = require("uuid");
const Permissions_1 = require("../../authorization/permissions/Permissions");
const ConversionUtil_1 = require("../../storage/conversion/ConversionUtil");
const UnprocessableEntityHttpError_1 = require("../../util/errors/UnprocessableEntityHttpError");
const IdentifierMap_1 = require("../../util/map/IdentifierMap");
const PathUtil_1 = require("../../util/PathUtil");
const StreamUtil_1 = require("../../util/StreamUtil");
const StringUtil_1 = require("../../util/StringUtil");
const Vocabularies_1 = require("../../util/Vocabularies");
const Notification_1 = require("./Notification");
var namedNode = n3_1.DataFactory.namedNode;
/**
 * All the necessary fields of the default features that are possible for all Notification Channels.
 */
const featureDefinitions = [
    { predicate: Vocabularies_1.NOTIFY.terms.accept, key: 'accept', dataType: Vocabularies_1.XSD.string },
    { predicate: Vocabularies_1.NOTIFY.terms.endAt, key: 'endAt', dataType: Vocabularies_1.XSD.dateTime },
    { predicate: Vocabularies_1.NOTIFY.terms.rate, key: 'rate', dataType: Vocabularies_1.XSD.duration },
    { predicate: Vocabularies_1.NOTIFY.terms.startAt, key: 'startAt', dataType: Vocabularies_1.XSD.dateTime },
    { predicate: Vocabularies_1.NOTIFY.terms.state, key: 'state', dataType: Vocabularies_1.XSD.string },
];
/**
 * The default notification features that are available on all channel types.
 */
exports.DEFAULT_NOTIFICATION_FEATURES = featureDefinitions.map((feat) => feat.predicate.value);
// This context is slightly outdated but seems to be the only "official" source for a SHACL context.
const CONTEXT_SHACL = 'https://w3c.github.io/shacl/shacl-jsonld-context/shacl.context.ld.json';
/**
 * The SHACL shape for the minimum requirements on a notification channel subscription request.
 */
exports.DEFAULT_SUBSCRIPTION_SHACL = {
    '@context': [CONTEXT_SHACL],
    '@type': 'sh:NodeShape',
    // Use the topic predicate to find the focus node
    targetSubjectsOf: Vocabularies_1.NOTIFY.topic,
    closed: true,
    property: [
        { path: Vocabularies_1.RDF.type, minCount: 1, maxCount: 1, nodeKind: 'sh:IRI' },
        { path: Vocabularies_1.NOTIFY.topic, minCount: 1, maxCount: 1, nodeKind: 'sh:IRI' },
        ...featureDefinitions.map((feat) => ({ path: feat.predicate.value, maxCount: 1, datatype: feat.dataType })),
    ],
};
/**
 * A {@link NotificationChannelType} that handles the base case of parsing and serializing a notification channel.
 * Note that the `extractModes` call always requires Read permissions on the target resource.
 *
 * Uses SHACL to validate the incoming data in `initChannel`.
 * Classes extending this can pass extra SHACL properties in the constructor to extend the validation check.
 *
 * The `completeChannel` implementation is an empty function.
 */
class BaseChannelType {
    /**
     * @param type - The URI of the notification channel type.
     *               This will be added to the SHACL shape to validate incoming subscription data.
     * @param route - The route corresponding to the URL of the subscription service of this channel type.
     *                Channel identifiers will be generated by appending a value to this URL.
     * @param features - The features that should be enabled for this channel type.
     *                   Values are expected to be full URIs, but the `notify:` prefix can also be used.
     * @param additionalShaclProperties - Any additional properties that need to be added to the default SHACL shape.
     */
    constructor(type, route, features = exports.DEFAULT_NOTIFICATION_FEATURES, additionalShaclProperties = []) {
        this.type = type;
        this.path = route.getPath();
        this.features = features.map((feature) => {
            if (feature.startsWith('notify:')) {
                feature = `${Vocabularies_1.NOTIFY.namespace}${feature.slice('notify:'.length)}`;
            }
            return namedNode(feature);
        });
        // Inject requested properties into default SHACL shape
        this.shacl = {
            ...exports.DEFAULT_SUBSCRIPTION_SHACL,
            property: [
                ...exports.DEFAULT_SUBSCRIPTION_SHACL.property,
                // Add type check
                { path: Vocabularies_1.RDF.type, hasValue: { '@id': type.value } },
                ...additionalShaclProperties,
            ],
        };
    }
    getDescription() {
        return {
            '@context': [Notification_1.CONTEXT_NOTIFICATION],
            id: this.path,
            // At the time of writing, there is no base value for URIs in the notification context,
            // so we use the full URI instead.
            channelType: this.type.value,
            // Shorten known features to make the resulting JSON more readable
            feature: this.features.map((node) => {
                if (exports.DEFAULT_NOTIFICATION_FEATURES.includes(node.value)) {
                    return node.value.slice(Vocabularies_1.NOTIFY.namespace.length);
                }
                if (node.value.startsWith(Vocabularies_1.NOTIFY.namespace)) {
                    return `notify:${node.value.slice(Vocabularies_1.NOTIFY.namespace.length)}`;
                }
                return node.value;
            }),
        };
    }
    /**
     * Initiates the channel by first calling {@link validateSubscription} followed by {@link quadsToChannel}.
     * Subclasses can override either function safely to impact the result of the function.
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async initChannel(data, credentials) {
        const subject = await this.validateSubscription(data);
        return this.quadsToChannel(data, subject);
    }
    /**
     * Returns an N3.js {@link Store} containing quads corresponding to the stored SHACL representation.
     * Caches this result so the conversion from JSON-LD to quads only has to happen once.
     */
    async getShaclQuads() {
        if (!this.shaclQuads) {
            const shaclStream = rdf_parse_1.default.parse(stream_1.Readable.from(JSON.stringify(this.shacl)), {
                contentType: 'application/ld+json',
                // Make sure our internal version of the context gets used
                [context_entries_1.KeysRdfParseJsonLd.documentLoader.name]: new ConversionUtil_1.ContextDocumentLoader({
                    [CONTEXT_SHACL]: '@css:templates/contexts/shacl.jsonld',
                }),
            });
            this.shaclQuads = await (0, StreamUtil_1.readableToQuads)(shaclStream);
        }
        return this.shaclQuads;
    }
    /**
     * Validates whether the given data conforms to the stored SHACL shape.
     * Will throw an {@link UnprocessableEntityHttpError} if validation fails.
     * Along with the SHACL check, this also makes sure there is only one matching entry in the dataset.
     *
     * @param data - The data to validate.
     *
     * @returns The focus node that corresponds to the subject of the found notification channel description.
     */
    async validateSubscription(data) {
        // Need to make sure there is exactly one matching entry, which can't be done with SHACL.
        // The predicate used here must be the same as is used for `targetSubjectsOf` in the SHACL shape.
        const focusNodes = data.getSubjects(Vocabularies_1.NOTIFY.terms.topic, null, null);
        if (focusNodes.length === 0) {
            throw new UnprocessableEntityHttpError_1.UnprocessableEntityHttpError('Missing topic value.');
        }
        if (focusNodes.length > 1) {
            throw new UnprocessableEntityHttpError_1.UnprocessableEntityHttpError('Only one subscription can be done at the same time.');
        }
        const validator = new rdf_validate_shacl_1.default(await this.getShaclQuads());
        const report = validator.validate(data);
        if (!report.conforms) {
            // Use the first error to generate error message
            const result = report.results[0];
            const message = result.message[0];
            throw new UnprocessableEntityHttpError_1.UnprocessableEntityHttpError(`${message.value} - ${result.path?.value}`);
        }
        // From this point on, we can assume the subject corresponds to a valid subscription request
        return focusNodes[0];
    }
    /**
     * Converts a set of quads to a {@link NotificationChannel}.
     * Assumes the data is valid, so this should be called after {@link validateSubscription}.
     *
     * The generated identifier will be a URL made by combining the base URL of the channel type with a unique identifier.
     *
     * The values of the default features will be added to the resulting channel,
     * subclasses with additional features that need to be added are responsible for parsing those quads.
     *
     * @param data - Data to convert.
     * @param subject - The identifier of the notification channel description in the dataset.
     *
     * @returns The generated {@link NotificationChannel}.
     */
    async quadsToChannel(data, subject) {
        const topic = data.getObjects(subject, Vocabularies_1.NOTIFY.terms.topic, null)[0];
        const type = data.getObjects(subject, Vocabularies_1.RDF.terms.type, null)[0];
        const channel = {
            id: (0, PathUtil_1.joinUrl)(this.path, (0, uuid_1.v4)()),
            type: type.value,
            topic: topic.value,
        };
        // Apply the values for all present features that are enabled
        for (const feature of exports.DEFAULT_NOTIFICATION_FEATURES) {
            const featNode = this.features.find((node) => node.value === feature);
            if (featNode) {
                const objects = data.getObjects(subject, feature, null);
                if (objects.length === 1) {
                    // Will always succeed since we are iterating over a list which was built using `featureDefinitions`
                    const { dataType, key } = featureDefinitions.find((feat) => feat.predicate.value === feature);
                    let val = objects[0].value;
                    if (dataType === Vocabularies_1.XSD.dateTime) {
                        val = Date.parse(val);
                    }
                    else if (dataType === Vocabularies_1.XSD.duration) {
                        val = (0, iso8601_duration_1.toSeconds)((0, iso8601_duration_1.parse)(val)) * 1000;
                    }
                    // Need to convince TS that we can assign `string | number` to this key
                    channel[key] = val;
                }
            }
        }
        return channel;
    }
    /**
     * Converts the given channel to a JSON-LD description.
     * All fields found in the channel, except `lastEmit`, will be part of the result subject,
     * so subclasses should remove any fields that should not be exposed.
     */
    async toJsonLd(channel) {
        const result = {
            '@context': [
                Notification_1.CONTEXT_NOTIFICATION,
            ],
            ...channel,
        };
        // No need to expose this field
        delete result.lastEmit;
        // Convert all the epoch values back to the expected date/rate format
        for (const { key, dataType } of featureDefinitions) {
            const value = channel[key];
            if (value) {
                if (dataType === Vocabularies_1.XSD.dateTime) {
                    result[key] = new Date(value).toISOString();
                }
                else if (dataType === Vocabularies_1.XSD.duration) {
                    result[key] = (0, StringUtil_1.msToDuration)(value);
                }
            }
        }
        return result;
    }
    async extractModes(channel) {
        return new IdentifierMap_1.IdentifierSetMultiMap([[{ path: channel.topic }, Permissions_1.AccessMode.read]]);
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async completeChannel(channel) {
        // Do nothing
    }
}
exports.BaseChannelType = BaseChannelType;
//# sourceMappingURL=BaseChannelType.js.map