@fraktalio/fmodel-ts
Version:
Functional domain modeling with TypeScript. Optimized for event sourcing and CQRS
237 lines • 13.6 kB
JavaScript
/*
* Copyright 2023 Fraktalio D.O.O. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
* AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
/* eslint-disable functional/no-mixed-types,functional/no-classes */
/**
* `_View` is a datatype that represents the event handling algorithm,
* responsible for translating the events into denormalized state,
* which is more adequate for querying.
*
* @typeParam Si - input State
* @typeParam So - output State
* @typeParam E - Event
*
* @param evolve - A function/lambda that takes input state of type `Si` and input event of type `Ei` as parameters, and returns the output/new state `So`
* @param initialState - A starting point / An initial state of type `So`
*
* `_View` is a pure `internal` domain component.
* It is not exported, and rather used to differentiate covariant and contravariant parameters, so we can map over them correctly.
*
* `_View` is used to model simpler (two parameter) public `View` component that has more practical usage.
* `View` has two type parameters: `S`, `E`, in where `S` = `Si` = `So`
*
* @author Иван Дугалић / Ivan Dugalic / @idugalic
*/
class _View {
constructor(evolve, initialState) {
this.evolve = evolve;
this.initialState = initialState;
}
/**
* Contra (Left) map on E/Event parameter - Contravariant
*
* @typeParam En - New Event
*/
mapContraOnEvent(f) {
return new _View((s, en) => this.evolve(s, f(en)), this.initialState);
}
/**
* Dimap on S/State parameter - Contravariant on the Si (input State) - Covariant on the So (output State) = Profunctor
*
* @typeParam Sin - New input State
* @typeParam Son - New output State
*/
dimapOnState(fl, fr) {
return new _View((s, e) => fr(this.evolve(fl(s), e)), fr(this.initialState));
}
/**
* Contra (Left) map on S/State parameter - Contravariant
*
* @typeParam Sin - New input State
*/
mapContraOnState(f) {
return this.dimapOnState(f, identity);
}
/**
* (Right) map on S/State parameter - Covariant
*
* @typeParam Son - New output State
*/
mapOnState(f) {
return this.dimapOnState(identity, f);
}
/**
* Right apply on S/State parameter - Applicative
*
* @typeParam Son - New output State
*/
applyOnState(ff) {
return new _View((s, e) => ff.evolve(s, e)(this.evolve(s, e)), ff.initialState(this.initialState));
}
/**
* Right product on S/State parameter - Applicative
*
* Combines state via intersection (So & Son)
*
* @typeParam Son - New output State
*/
productOnState(fb) {
return this.applyOnState(fb.mapOnState((b) => (a) => ({ ...a, ...b })));
}
/**
* Right product on S/State parameter - Applicative
*
* Combines state via tuple [So, Son]
*
* @typeParam Son - New output State
*/
productViaTuplesOnState(fb) {
return this.applyOnState(fb.mapOnState((b) => (a) => [a, b]));
}
/**
* Combines multiple Views into one View.
*
* Combines state via intersection (So & Son)
*
*/
combine(y) {
const viewX = this.mapContraOnEvent((en) => en).mapContraOnState((sin) => sin);
const viewY = y
.mapContraOnEvent((en2) => en2)
.mapContraOnState((sin) => sin);
return viewX.productOnState(viewY);
}
/**
* Combines multiple Views into one View.
*
* Combines state via tuple [So, Son].
*
*/
combineViaTuples(y) {
const viewX = this.mapContraOnEvent((en) => en).mapContraOnState((sin) => sin[0]);
const viewY = y
.mapContraOnEvent((en2) => en2)
.mapContraOnState((sin) => sin[1]);
return viewX.productViaTuplesOnState(viewY);
}
}
/**
* `View` is a datatype that represents the event handling algorithm,
* responsible for translating the events into denormalized state,
* which is more adequate for querying.
*
* @typeParam S - State
* @typeParam E - Event
*
* ### Example
* ```typescript
* export const orderView: View<OrderView | null, OrderEvent> = new View<
* OrderView | null,
* OrderEvent
* >(
* (currentState, event) => {
* switch (event.kind) {
* case "OrderCreatedEvent":
* return {
* orderId: event.id,
* restaurantId: event.restaurantId,
* menuItems: event.menuItems,
* status: "CREATED",
* };
* case "OrderNotCreatedEvent":
* return currentState;
* case "OrderPreparedEvent":
* return currentState !== null
* ? {
* orderId: currentState.orderId,
* restaurantId: currentState.restaurantId,
* menuItems: currentState.menuItems,
* status: "PREPARED",
* }
* : currentState;
* case "OrderNotPreparedEvent":
* return currentState;
* default:
* // Exhaustive matching of the event type
* const _: never = event;
* return currentState;
* }
* },
* null,
* );
* ```
*
* @author Иван Дугалић / Ivan Dugalic / @idugalic
*/
export class View {
constructor(evolve, initialState) {
this.evolve = evolve;
this.initialState = initialState;
}
/**
* Contra (Left) map on E/Event parameter - Contravariant
*
* @typeParam En - New Event
*/
mapContraOnEvent(f) {
return asView(new _View(this.evolve, this.initialState).mapContraOnEvent(f));
}
/**
* Dimap on S/State parameter - Profunctor
*
* @typeParam Sn - New State
*/
dimapOnState(fl, fr) {
return asView(new _View(this.evolve, this.initialState).dimapOnState(fl, fr));
}
/**
* Combines Views into one bigger View - Monoid
*
* Combines state via intersection (S & S2). Check alternative method `combineViaTuples`.
*
* 1. Flexibility: If you anticipate needing to access individual components of the combined state separately, using tuples might be more appropriate, as it allows you to maintain separate types for each component. However, if you primarily need to treat the combined state as a single entity with all properties accessible at once, intersections might be more suitable.
*
* 2. Readability: Consider which approach makes your code more readable and understandable to other developers who may be working with your codebase. Choose the approach that best communicates your intentions and the structure of your data.
*
* 3. Compatibility: Consider the compatibility of your chosen approach with other libraries, frameworks, or tools you're using in your TypeScript project. Some libraries or tools might work better with one approach over the other.
*/
combine(view2) {
return asView(new _View(this.evolve, this.initialState).combine(new _View(view2.evolve, view2.initialState)));
}
/**
* Combines Views into one bigger View - Monoid
*
* Combines state via tuple [S, S2]. Check alternative method `combine`
*
* 1. Flexibility: If you anticipate needing to access individual components of the combined state separately, using tuples might be more appropriate, as it allows you to maintain separate types for each component. However, if you primarily need to treat the combined state as a single entity with all properties accessible at once, intersections might be more suitable.
*
* 2. Readability: Consider which approach makes your code more readable and understandable to other developers who may be working with your codebase. Choose the approach that best communicates your intentions and the structure of your data.
*
* 3. Compatibility: Consider the compatibility of your chosen approach with other libraries, frameworks, or tools you're using in your TypeScript project. Some libraries or tools might work better with one approach over the other.
*/
combineViaTuples(view2) {
return asView(new _View(this.evolve, this.initialState).combineViaTuples(new _View(view2.evolve, view2.initialState)));
}
}
/**
* Identity function
*/
const identity = (t) => t;
/**
* Creates `View` from internal `_View`
*
* @param view
*/
function asView(view) {
return new View(view.evolve, view.initialState);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmlldy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvZG9tYWluL3ZpZXcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7O0dBV0c7QUFFSCxvRUFBb0U7QUFFcEU7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7QUFDSCxNQUFNLEtBQUs7SUFDVCxZQUNXLE1BQTJCLEVBQzNCLFlBQWdCO1FBRGhCLFdBQU0sR0FBTixNQUFNLENBQXFCO1FBQzNCLGlCQUFZLEdBQVosWUFBWSxDQUFJO0lBQ3hCLENBQUM7SUFFSjs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQUssQ0FBZ0I7UUFDbkMsT0FBTyxJQUFJLEtBQUssQ0FDZCxDQUFDLENBQUssRUFBRSxFQUFNLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUN4QyxJQUFJLENBQUMsWUFBWSxDQUNsQixDQUFDO0lBQ0osQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0gsWUFBWSxDQUNWLEVBQW9CLEVBQ3BCLEVBQW1CO1FBRW5CLE9BQU8sSUFBSSxLQUFLLENBQ2QsQ0FBQyxDQUFNLEVBQUUsQ0FBSSxFQUFFLEVBQUUsQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFDM0MsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FDdEIsQ0FBQztJQUNKLENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsZ0JBQWdCLENBQU0sQ0FBbUI7UUFDdkMsT0FBTyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUMsRUFBRSxRQUFRLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILFVBQVUsQ0FBTSxDQUFrQjtRQUNoQyxPQUFPLElBQUksQ0FBQyxZQUFZLENBQUMsUUFBUSxFQUFFLENBQUMs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