Source: client/actor-request-factory.js

  1. /*
  2. * Copyright 2023, TeamDev. All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Redistribution and use in source and/or binary forms, with or without
  11. * modification, must retain the above copyright notice and the following
  12. * disclaimer.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  15. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  16. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  17. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  18. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  19. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  20. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  21. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  22. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  23. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  24. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. "use strict";
  27. import {v4 as newUuid} from 'uuid';
  28. import {Message} from 'google-protobuf';
  29. import {FieldMask} from '../proto/google/protobuf/field_mask_pb';
  30. import {Timestamp} from '../proto/google/protobuf/timestamp_pb';
  31. import {
  32. CompositeFilter,
  33. Filter,
  34. IdFilter,
  35. Target,
  36. TargetFilters
  37. } from '../proto/spine/client/filters_pb';
  38. import {OrderBy, Query, QueryId, ResponseFormat} from '../proto/spine/client/query_pb';
  39. import {Topic, TopicId} from '../proto/spine/client/subscription_pb';
  40. import {ActorContext} from '../proto/spine/core/actor_context_pb';
  41. import {Command, CommandContext, CommandId} from '../proto/spine/core/command_pb';
  42. import {UserId} from '../proto/spine/core/user_id_pb';
  43. import {ZoneId, ZoneOffset} from '../proto/spine/time/time_pb';
  44. import {isProtobufMessage, Type, TypedMessage} from './typed-message';
  45. import {AnyPacker} from './any-packer';
  46. import {FieldPaths} from './field-paths';
  47. import {EnumValue} from 'google-protobuf/google/protobuf/type_pb';
  48. /**
  49. * Wraps the passed enum value as Protobuf `EnumValue` so it can be correctly processed by the
  50. * server.
  51. *
  52. * As enums in Protobuf JS are declared as plain `number`s, their values can be passed to this
  53. * method as-is, for example: `enumValueOf(Task.Severity.HIGH)`.
  54. *
  55. * @param {!number} value the enum value
  56. * @returns {EnumValue} the `EnumValue` instance
  57. */
  58. export function enumValueOf(value) {
  59. const result = new EnumValue();
  60. result.setNumber(value);
  61. return result;
  62. }
  63. const ENUM_VALUE_TYPE_URL = 'type.googleapis.com/google.protobuf.EnumValue';
  64. // TODO:2019-06-07:yegor.udovchenko: Cover `Filters` class with the unit tests
  65. // https://github.com/SpineEventEngine/web/issues/100
  66. /**
  67. * A factory for `Filter` and `CompositeFilter` instances.
  68. */
  69. export class Filters {
  70. /**
  71. * @typedef {string | number | boolean | Date | TypedMessage<T> | Message} FieldValue
  72. *
  73. * Represents all types acceptable as a value for filtering.
  74. *
  75. * @template <T> a type of the Protobuf message to compare with
  76. */
  77. /**
  78. * Instantiation not allowed and will throw an error.
  79. */
  80. constructor() {
  81. throw new Error('Tried instantiating a utility class.');
  82. }
  83. /**
  84. * Creates a new filter for the value of an object field to be equal to the provided value.
  85. *
  86. * @param {!String} fieldPath a path to the object field
  87. * @param {!FieldValue} value a value to compare with
  88. *
  89. * @return {Filter} a new filter instance
  90. */
  91. static eq(fieldPath, value) {
  92. return Filters.with(fieldPath, Filter.Operator.EQUAL, value);
  93. }
  94. /**
  95. * Creates a new filter for the value of an object field to be less than the provided value.
  96. *
  97. * @param {!String} fieldPath a path to the object field
  98. * @param {!FieldValue} value a value to compare with
  99. *
  100. * @return {Filter} a new filter instance
  101. */
  102. static lt(fieldPath, value) {
  103. return Filters.with(fieldPath, Filter.Operator.LESS_THAN, value);
  104. }
  105. /**
  106. * Creates a new filter for the value an object field to be greater than the provided value.
  107. *
  108. * @param {!String} fieldPath a path to the object field
  109. * @param {!FieldValue} value a value to compare with
  110. *
  111. * @return {Filter} a new filter instance
  112. */
  113. static gt(fieldPath, value) {
  114. return Filters.with(fieldPath, Filter.Operator.GREATER_THAN, value);
  115. }
  116. /**
  117. * Creates a new filter for the value of an object field to be less or equal compared to
  118. * the provided value.
  119. *
  120. * @param {!String} fieldPath a path to the object field
  121. * @param {!FieldValue} value a value to compare with
  122. *
  123. * @return {Filter} a new filter instance
  124. */
  125. static le(fieldPath, value) {
  126. return Filters.with(fieldPath, Filter.Operator.LESS_OR_EQUAL, value);
  127. }
  128. /**
  129. * Creates a new filter for the value of an object field to be greater or equal compared to
  130. * the provided value.
  131. *
  132. * @param {!String} fieldPath a path to the object field
  133. * @param {!FieldValue} value a value to compare with
  134. *
  135. * @return {Filter} a new filter instance
  136. */
  137. static ge(fieldPath, value) {
  138. return Filters.with(fieldPath, Filter.Operator.GREATER_OR_EQUAL, value);
  139. }
  140. /**
  141. * Creates a filter for an object field to match the provided value according to an operator.
  142. *
  143. * Accepts various types of {@link FieldValue field values}.
  144. *
  145. * @example
  146. * // Create filters with primitive values to compare
  147. * Filters.eq('description', 'Sample task description') // Wraps string in the Protobuf `StringValue`
  148. * Filters.gt('length', 12) // Wraps number in the Protobuf `Int32Value`
  149. * Filters.eq('multiline', false) // Wraps boolean in the Protobuf `BoolValue`
  150. *
  151. * @example
  152. * // Create filter for the primitive value of a custom type
  153. * Filters.gt('price', TypedMessage.float(7.41))
  154. *
  155. * @example
  156. * // Create filter for the time-based value
  157. * Filters.gt('whenCreated', new Date(2019, 5, 4)) // Converts the given date to the `Timestamp` message
  158. *
  159. * @example
  160. * // Create filter for the user-defined type
  161. * Filters.eq('status', Task.Status.COMPLETED)
  162. *
  163. * @param {!String} fieldPath a path to the object field
  164. * @param {!Filter.Operator} operator an operator to check the field value upon
  165. * @param {!FieldValue} value a value to compare the field value to
  166. *
  167. * @return {Filter} a new filter instance
  168. */
  169. static with(fieldPath, operator, value) {
  170. let typedValue;
  171. if (value instanceof Number || typeof value === 'number') {
  172. typedValue = TypedMessage.int32(value);
  173. } else if (value instanceof String || typeof value === 'string') {
  174. typedValue = TypedMessage.string(value);
  175. } else if (value instanceof Boolean || typeof value === 'boolean') {
  176. typedValue = TypedMessage.bool(value);
  177. } else if (value instanceof Date) {
  178. typedValue = TypedMessage.timestamp(value);
  179. } else if (value instanceof EnumValue) {
  180. const type = Type.of(EnumValue, ENUM_VALUE_TYPE_URL);
  181. typedValue = new TypedMessage(value, type);
  182. } else if (value instanceof TypedMessage) {
  183. typedValue = value;
  184. } else if(isProtobufMessage(value)) {
  185. typedValue = TypedMessage.of(value);
  186. } else {
  187. throw new Error(`Unable to create filter.
  188. Filter value type of ${typeof value} is unsupported.`)
  189. }
  190. const wrappedValue = AnyPacker.packTyped(typedValue);
  191. const filter = new Filter();
  192. filter.setFieldPath(FieldPaths.parse(fieldPath));
  193. filter.setValue(wrappedValue);
  194. filter.setOperator(operator);
  195. return filter;
  196. }
  197. /**
  198. * Creates a new composite filter which matches objects that fit every provided filter.
  199. *
  200. * @param {!Filter[]} filters an array of simple filters
  201. *
  202. * @return {CompositeFilter} a new composite filter with `ALL` operator
  203. */
  204. static all(filters) {
  205. return Filters.compose(filters, CompositeFilter.CompositeOperator.ALL);
  206. }
  207. /**
  208. * Creates a new composite filter which matches objects that fit at least one
  209. * of the provided filters.
  210. *
  211. * @param {!Filter[]} filters an array of simple filters
  212. *
  213. * @return {CompositeFilter} a new composite filter with `EITHER` operator
  214. */
  215. static either(filters) {
  216. return Filters.compose(filters, CompositeFilter.CompositeOperator.EITHER);
  217. }
  218. /**
  219. * Creates a new composite filter which matches objects according to an array of filters with a
  220. * specified logical operator.
  221. *
  222. * @param {!Filter[]} filters an array of simple filters
  223. * @param {!CompositeFilter.CompositeOperator} operator a logical operator for `filters`
  224. *
  225. * @return {CompositeFilter} a new composite filter
  226. */
  227. static compose(filters, operator) {
  228. const compositeFilter = new CompositeFilter();
  229. compositeFilter.setFilterList(filters);
  230. compositeFilter.setOperator(operator);
  231. return compositeFilter;
  232. }
  233. }
  234. /**
  235. * Utilities for working with `Query` and `Topic` targets.
  236. */
  237. class Targets {
  238. /**
  239. * Instantiation not allowed and will throw an error.
  240. */
  241. constructor() {
  242. throw new Error('Tried instantiating a utility class.');
  243. }
  244. /**
  245. * Composes a new target for objects of specified type, optionally with specified IDs and
  246. * filters.
  247. *
  248. * @param {!Type} type a Type URL of target objects
  249. * @param {?TypedMessage[]} ids an array of IDs one of which must be matched by each target
  250. * object
  251. * @param {?CompositeFilter[]} filters an array of filters target
  252. *
  253. * @return {Target} a newly created target for objects matching the specified filters
  254. */
  255. static compose({forType: type, withIds: ids, filteredBy: filters}) {
  256. const includeAll = !ids && !filters;
  257. if (includeAll) {
  258. return Targets._all(type);
  259. }
  260. const targetFilters = new TargetFilters();
  261. const idList = Targets._nullToEmpty(ids);
  262. if (idList.length) {
  263. const idFilter = Targets._assembleIdFilter(idList);
  264. targetFilters.setIdFilter(idFilter);
  265. }
  266. const filterList = Targets._nullToEmpty(filters);
  267. if (filterList) {
  268. targetFilters.setFilterList(filterList);
  269. }
  270. return Targets._filtered(type, targetFilters);
  271. }
  272. /**
  273. * Creates a new target including all items of type.
  274. *
  275. * @param {!Type} type
  276. * @return {Target}
  277. * @private
  278. */
  279. static _all(type) {
  280. const target = new Target();
  281. target.setType(type.url().value());
  282. target.setIncludeAll(true);
  283. return target;
  284. }
  285. /**
  286. * Creates a new target including only items of the specified type that pass filtering.
  287. *
  288. * @param {!Type} type
  289. * @param {!TargetFilters} filters
  290. * @return {Target}
  291. * @private
  292. */
  293. static _filtered(type, filters) {
  294. const target = new Target();
  295. target.setType(type.url().value());
  296. target.setFilters(filters);
  297. return target;
  298. }
  299. /**
  300. * Creates a targets ID filter including only items which are included in the provided ID list.
  301. *
  302. * @param {!TypedMessage[]} ids an array of IDs for items matching target to be included in
  303. * @return {IdFilter}
  304. * @private
  305. */
  306. static _assembleIdFilter(ids) {
  307. const idFilter = new IdFilter();
  308. ids.forEach(rawId => {
  309. const packedId = AnyPacker.packTyped(rawId);
  310. idFilter.addId(packedId);
  311. });
  312. return idFilter;
  313. }
  314. /**
  315. * @param {?T[]} input
  316. * @return {T[]} an empty array if the value is `null`, or the provided input otherwise
  317. * @template <T> type of items in the provided array
  318. * @private
  319. */
  320. static _nullToEmpty(input) {
  321. if (input == null) {
  322. return [];
  323. } else {
  324. return input;
  325. }
  326. }
  327. }
  328. const INVALID_FILTER_TYPE =
  329. 'All filters passed to QueryFilter#where() must be of a single type: ' +
  330. 'either Filter or CompositeFilter.';
  331. /**
  332. * An abstract base for builders that create `Message` instances which have a `Target`
  333. * and a `FieldMask` as attributes.
  334. *
  335. * <p>The `Target` matching the builder configuration is accessed with `#getTarget()`,
  336. * while the `FieldMask` is retrieved with `#getMask()`.
  337. *
  338. * The public API of this class is inspired by the SQL syntax.
  339. * ```javascript
  340. * select(CUSTOMER_TYPE) // returning <AbstractTargetBuilder> descendant instance
  341. * .byIds(getWestCoastCustomerIds())
  342. * .withMask(["name", "address", "email"])
  343. * .where([
  344. * Filters.eq("type", "permanent"),
  345. * Filters.eq("discountPercent", 10),
  346. * Filters.eq("companySize", Company.Size.SMALL)
  347. * ])
  348. * .build()
  349. * ```
  350. *
  351. * @template <T>
  352. * a type of the message which is returned by the implementations `#build()`
  353. * @abstract
  354. */
  355. class AbstractTargetBuilder {
  356. /**
  357. * @param {!Class<Message>} entity a Protobuf type of the target entities
  358. */
  359. constructor(entity) {
  360. /**
  361. * A type composed from the target entity class.
  362. *
  363. * @type {Type}
  364. * @private
  365. */
  366. this._type = Type.forClass(entity);
  367. /**
  368. * @type {TypedMessage[]}
  369. * @private
  370. */
  371. this._ids = null;
  372. /**
  373. * @type {CompositeFilter[]}
  374. * @private
  375. */
  376. this._filters = null;
  377. /**
  378. * @type {FieldMask}
  379. * @private
  380. */
  381. this._fieldMask = null;
  382. }
  383. /**
  384. * Sets an ID predicate of the `Query#getTarget()`.
  385. *
  386. * Makes the query return only the items identified by the provided IDs.
  387. *
  388. * Supported ID types are string, number, and Protobuf messages. All of the passed
  389. * IDs must be of the same type.
  390. *
  391. * If number IDs are passed they are assumed to be of `int64` Protobuf type.
  392. *
  393. * @param {!Message[]|!Number[]|!String[]} ids an array with identifiers to query
  394. * @return {this} the current builder instance
  395. * @throws if this method is executed more than once
  396. * @throws if the provided IDs are not an instance of `Array`
  397. * @throws if any of provided IDs are not an instance of supported types
  398. * @throws if the provided IDs are not of the same type
  399. */
  400. byIds(ids) {
  401. if (this._ids !== null) {
  402. throw new Error('Can not set query ID more than once for QueryBuilder.');
  403. }
  404. if (!(ids instanceof Array)) {
  405. throw new Error('Only an array of IDs is allowed as parameter to QueryBuilder#byIds().');
  406. }
  407. if (!ids.length) {
  408. return this;
  409. }
  410. const invalidTypeMessage = 'Each provided ID must be a string, number or a Protobuf message.';
  411. if (ids[0] instanceof Number || typeof ids[0] === 'number') {
  412. AbstractTargetBuilder._checkAllOfType(ids, Number, invalidTypeMessage);
  413. this._ids = ids.map(TypedMessage.int64);
  414. } else if (ids[0] instanceof String || typeof ids[0] === 'string') {
  415. AbstractTargetBuilder._checkAllOfType(ids, String, invalidTypeMessage);
  416. this._ids = ids.map(TypedMessage.string);
  417. } else if (!isProtobufMessage(ids[0])){
  418. throw new Error(invalidTypeMessage);
  419. } else {
  420. AbstractTargetBuilder._checkAllOfType(ids, ids[0].constructor, invalidTypeMessage);
  421. this._ids = ids.map(id => TypedMessage.of(id));
  422. }
  423. return this;
  424. }
  425. /**
  426. * Sets a field value predicate of the `Query#getTarget()`.
  427. *
  428. * <p>If there are no `Filter`s (i.e. the provided array is empty), all
  429. * the records will be returned by executing the `Query`.
  430. *
  431. * <p>An array of predicates provided to this method are considered to be joined in
  432. * a conjunction (using `CompositeFilter.CompositeOperator#ALL`). This means
  433. * a record would match this query only if it matches all of the predicates.
  434. *
  435. * @param {!Filter[]|CompositeFilter[]} predicates
  436. * the predicates to filter the requested items by
  437. * @return {this} self for method chaining
  438. * @throws if this method is executed more than once
  439. * @see Filters a convenient way to create `Filter` instances
  440. */
  441. where(predicates) {
  442. if (this._filters !== null) {
  443. throw new Error('Can not set filters more than once for QueryBuilder.');
  444. }
  445. if (!(predicates instanceof Array)) {
  446. throw new Error('Only an array of predicates is allowed as parameter to QueryBuilder#where().');
  447. }
  448. if (!predicates.length) {
  449. return this;
  450. }
  451. if (predicates[0] instanceof Filter) {
  452. AbstractTargetBuilder._checkAllOfType(predicates, Filter, INVALID_FILTER_TYPE);
  453. const aggregatingFilter = Filters.all(predicates);
  454. this._filters = [aggregatingFilter];
  455. } else {
  456. AbstractTargetBuilder._checkAllOfType(predicates, CompositeFilter, INVALID_FILTER_TYPE);
  457. this._filters = predicates.slice();
  458. }
  459. return this;
  460. }
  461. /**
  462. * Sets a Field Mask of the `Query`.
  463. *
  464. * The names of the fields must be formatted according to the `FieldMask`
  465. * specification.
  466. *
  467. * If there are no fields (i.e. an empty array is passed), all the fields will
  468. * be returned by query.
  469. *
  470. * @param {!String[]} fieldNames
  471. * @return {this} self for method chaining
  472. * @throws if this method is executed more than once
  473. * @see FieldMask specification for `FieldMask`
  474. */
  475. withMask(fieldNames) {
  476. if (this._fieldMask != null) {
  477. throw new Error('Can not set field mask more than once for QueryBuilder.');
  478. }
  479. if (!(fieldNames instanceof Array)) {
  480. throw new Error('Only an array of strings is allowed as parameter to QueryBuilder#withMask().');
  481. }
  482. AbstractTargetBuilder._checkAllOfType(fieldNames, String, 'Field names should be strings.');
  483. if (!fieldNames.length) {
  484. return this;
  485. }
  486. this._fieldMask = new FieldMask();
  487. this._fieldMask.setPathsList(fieldNames);
  488. return this;
  489. }
  490. /**
  491. * @return {Target} a target matching builders configuration
  492. */
  493. getTarget() {
  494. return this._buildTarget();
  495. }
  496. /**
  497. * Creates a new target `Target` instance based on this builder configuration.
  498. *
  499. * @return {Target} a new target
  500. */
  501. _buildTarget() {
  502. return Targets.compose({forType: this._type, withIds: this._ids, filteredBy: this._filters});
  503. }
  504. /**
  505. * @return {FieldMask} a fields mask set to this builder
  506. */
  507. getMask() {
  508. return this._fieldMask;
  509. }
  510. /**
  511. * A build method for creating instances of this builders target class.
  512. *
  513. * @return {T} a new target class instance
  514. * @abstract
  515. */
  516. build() {
  517. throw new Error('Not implemented in abstract base.');
  518. }
  519. /**
  520. * Checks that each provided item is an instance of the provided class. In case the check does
  521. * not pass an error is thrown.
  522. *
  523. * @param {!Array} items an array of objects that are expected to be of the provided type
  524. * @param {!Object} cls a class each item is required to be instance of
  525. * @param {!String} message an error message thrown on type mismatch
  526. * @private
  527. */
  528. static _checkAllOfType(items, cls, message = 'Unexpected parameter type.') {
  529. if (cls === String) {
  530. AbstractTargetBuilder._checkAllAreStrings(items, message);
  531. } else if (cls === Number) {
  532. AbstractTargetBuilder._checkAllAreNumbers(items, message);
  533. } else if (cls === Boolean) {
  534. AbstractTargetBuilder._checkAllAreBooleans(items, message);
  535. } else {
  536. AbstractTargetBuilder._checkAllOfClass(cls, items, message);
  537. }
  538. }
  539. /**
  540. * @param {!Array} items an array of objects that are expected to be strings
  541. * @param {!String} message an error message thrown on type mismatch
  542. * @private
  543. */
  544. static _checkAllAreStrings(items, message) {
  545. items.forEach(item => {
  546. if (typeof item !== 'string' && !(item instanceof String)) {
  547. throw new Error(message);
  548. }
  549. });
  550. }
  551. /**
  552. * @param {!Array} items an array of objects that are expected to be numbers
  553. * @param {!String} message an error message thrown on type mismatch
  554. * @private
  555. */
  556. static _checkAllAreNumbers(items, message) {
  557. items.forEach(item => {
  558. if (typeof item !== 'number' && !(item instanceof Number)) {
  559. throw new Error(message);
  560. }
  561. });
  562. }
  563. /**
  564. * @param {!Array} items an array of objects that are expected to be booleans
  565. * @param {!String} message an error message thrown on type mismatch
  566. * @private
  567. */
  568. static _checkAllAreBooleans(items, message) {
  569. items.forEach(item => {
  570. if (typeof item !== 'boolean' && !(item instanceof Boolean)) {
  571. throw new Error(message);
  572. }
  573. });
  574. }
  575. /**
  576. * @param {!Object} cls a class tyo check items against
  577. * @param {!Array} items an array of objects that are expected to instances of class
  578. * @param {!String} message an error message thrown on type mismatch
  579. * @private
  580. */
  581. static _checkAllOfClass(cls, items, message) {
  582. items.forEach(item => {
  583. if (!(item instanceof cls)) {
  584. throw new Error(message);
  585. }
  586. });
  587. }
  588. }
  589. /**
  590. * A builder for creating `Query` instances. A more flexible approach to query creation
  591. * than using a `QueryFactory`.
  592. *
  593. * @extends {AbstractTargetBuilder<Query>}
  594. * @template <T> a Protobuf type of the query target entities
  595. */
  596. class QueryBuilder extends AbstractTargetBuilder {
  597. /**
  598. * @param {!Class<Message>} entity a Protobuf type of the query target entities
  599. * @param {!QueryFactory} queryFactory
  600. */
  601. constructor(entity, queryFactory) {
  602. super(entity);
  603. /**
  604. * @type {QueryFactory}
  605. * @private
  606. */
  607. this._factory = queryFactory;
  608. /**
  609. * @type {number}
  610. * @private
  611. */
  612. this._limit = 0;
  613. /**
  614. * @type {OrderBy}
  615. * @private
  616. */
  617. this._orderBy = null;
  618. }
  619. /**
  620. * Limits the query response to the given number of entities.
  621. *
  622. * The value must be non-negative, otherwise an error occurs. If set to `0`, all the available
  623. * entities are retrieved.
  624. *
  625. * When set, the result ordering must also be specified.
  626. *
  627. * @param {number} limit the max number of response entities
  628. */
  629. limit(limit) {
  630. if (limit < 0) {
  631. throw new Error("Query limit must not be negative.");
  632. }
  633. this._limit = limit;
  634. return this;
  635. }
  636. /**
  637. * Requests the query results to be ordered by the given `column` in the descending direction.
  638. *
  639. * Whether the results will be sorted in the requested order depends on the implementation of
  640. * server-side communication. For example, the Firebase-based communication protocol does not
  641. * preserve ordering. Regardless, if a `limit` is set for a query, an ordering is also required.
  642. *
  643. * @param column
  644. */
  645. orderDescendingBy(column) {
  646. this._addOrderBy(column, OrderBy.Direction.DESCENDING);
  647. return this;
  648. }
  649. /**
  650. * Requests the query results to be ordered by the given `column` in the ascending direction.
  651. *
  652. * Whether the results will be sorted in the requested order depends on the implementation of
  653. * server-side communication. For example, the Firebase-based communication protocol does not
  654. * preserve ordering. Regardless, if a `limit` is set for a query, an ordering is also required.
  655. *
  656. * @param column
  657. */
  658. orderAscendingBy(column) {
  659. this._addOrderBy(column, OrderBy.Direction.ASCENDING);
  660. return this;
  661. }
  662. /**
  663. * Specifies the expected response ordering.
  664. *
  665. * @param column the name of the column to order by
  666. * @param direction the direction of ordering: `OrderBy.Direction.ASCENDING` or
  667. * `OrderBy.Direction.DESCENDING`
  668. * @private
  669. */
  670. _addOrderBy(column, direction) {
  671. if (column === null) {
  672. throw new Error("Column name must not be `null`.");
  673. }
  674. this._orderBy = new OrderBy();
  675. this._orderBy.setColumn(column);
  676. this._orderBy.setDirection(direction);
  677. }
  678. /**
  679. * Creates the Query instance based on the current builder configuration.
  680. *
  681. * @return {Query} a new query
  682. */
  683. build() {
  684. const target = this.getTarget();
  685. const fieldMask = this.getMask();
  686. const limit = this._limit;
  687. const order = this._orderBy;
  688. if (limit !== 0 && order === null) {
  689. throw Error("Ordering is required for queries with a `limit`.")
  690. }
  691. return this._factory.compose({forTarget: target,
  692. withMask: fieldMask,
  693. limit: limit,
  694. orderBy: order});
  695. }
  696. }
  697. /**
  698. * A factory for creating `Query` instances specifying the data to be retrieved from Spine server.
  699. *
  700. * @see ActorRequestFactory#query()
  701. * @template <T> a Protobuf type of the query target entities
  702. */
  703. class QueryFactory {
  704. /**
  705. * @param {!ActorRequestFactory} requestFactory
  706. */
  707. constructor(requestFactory) {
  708. this._requestFactory = requestFactory;
  709. }
  710. /**
  711. * Creates a new builder of `Query` instances of the provided type.
  712. *
  713. * @param {!Class<Message>} entity a Protobuf type of the query target entities
  714. * @return {QueryBuilder}
  715. */
  716. select(entity) {
  717. return new QueryBuilder(entity, this);
  718. }
  719. /**
  720. * Creates a new `Query` which would return only entities which conform the target specification.
  721. *
  722. * An optional field mask can be provided to specify particular fields to be returned for `Query`
  723. *
  724. * @param {!Target} forTarget a specification of type and filters for `Query` result to match
  725. * @param {?FieldMask} fieldMask a specification of fields to be returned by executing `Query`
  726. * @param {number} limit max number of entities to fetch
  727. * @return {Query}
  728. */
  729. compose({forTarget: target, withMask: fieldMask, limit: limit, orderBy: orderBy}) {
  730. return this._newQuery(target, fieldMask, limit, orderBy);
  731. }
  732. /**
  733. * @param {!Target} target a specification of type and filters for `Query` result to match
  734. * @param {?FieldMask} fieldMask a specification of fields to be returned by executing `Query`
  735. * @param {?Number} limit the maximum number of the requested entities; must go with `orderBy`
  736. * @param {?OrderBy} orderBy ordering of the resulting entities
  737. * @return {Query} a new query instance
  738. * @private
  739. */
  740. _newQuery(target, fieldMask, limit, orderBy) {
  741. const id = QueryFactory._newId();
  742. const actorContext = this._requestFactory._actorContext();
  743. const format = new ResponseFormat();
  744. format.setFieldMask(fieldMask);
  745. format.setLimit(limit);
  746. format.setOrderBy(orderBy);
  747. const result = new Query();
  748. result.setId(id);
  749. result.setTarget(target);
  750. result.setFormat(format);
  751. result.setContext(actorContext);
  752. return result;
  753. }
  754. /**
  755. * @return {QueryId}
  756. * @private
  757. */
  758. static _newId() {
  759. const result = new QueryId();
  760. result.setValue(`q-${newUuid()}`);
  761. return result;
  762. }
  763. }
  764. /**
  765. * A factory of `Command` instances.
  766. *
  767. * Uses the given `ActorRequestFactory` as the source of the command meta information,
  768. * such as the actor, the tenant, etc.
  769. *
  770. * @see ActorRequestFactory#command()
  771. */
  772. class CommandFactory {
  773. constructor(actorRequestFactory) {
  774. this._requestFactory = actorRequestFactory;
  775. }
  776. /**
  777. * Creates a `Command` from the given command message.
  778. *
  779. * @param {!Message} message a command message
  780. * @return {Command} a Spine Command
  781. */
  782. create(message) {
  783. const id = CommandFactory._newCommandId();
  784. const messageAny = AnyPacker.packMessage(message);
  785. const context = this._commandContext();
  786. const result = new Command();
  787. result.setId(id);
  788. result.setMessage(messageAny);
  789. result.setContext(context);
  790. return result;
  791. }
  792. _commandContext() {
  793. const result = new CommandContext();
  794. const actorContext = this._requestFactory._actorContext();
  795. result.setActorContext(actorContext);
  796. return result;
  797. }
  798. /**
  799. * @return {CommandId}
  800. * @private
  801. */
  802. static _newCommandId() {
  803. const result = new CommandId();
  804. result.setUuid(newUuid());
  805. return result;
  806. }
  807. }
  808. /**
  809. * A builder for creating `Topic` instances. A more flexible approach to query creation
  810. * than using a `TopicFactory`.
  811. *
  812. * @extends {AbstractTargetBuilder<Topic>}
  813. * @template <T> a Protobuf type of the subscription target entities
  814. */
  815. class TopicBuilder extends AbstractTargetBuilder {
  816. /**
  817. * @param {!Class<Message>} entity a Protobuf type of the subscription target entities
  818. * @param {!TopicFactory} topicFactory
  819. */
  820. constructor(entity, topicFactory) {
  821. super(entity);
  822. /**
  823. * @type {TopicFactory}
  824. * @private
  825. */
  826. this._factory = topicFactory;
  827. }
  828. /**
  829. * Creates the `Topic` instance based on the current builder configuration.
  830. *
  831. * @return {Topic} a new topic
  832. */
  833. build() {
  834. return this._factory.compose({
  835. forTarget: this.getTarget(),
  836. withMask: this.getMask(),
  837. });
  838. }
  839. }
  840. /**
  841. * A factory of {@link Topic} instances.
  842. *
  843. * Uses the given {@link ActorRequestFactory} as the source of the topic meta information,
  844. * such as the actor.
  845. *
  846. * @see ActorRequestFactory#topic()
  847. * @template <T> a Protobuf type of the subscription target entities
  848. */
  849. class TopicFactory {
  850. /**
  851. * @param {!ActorRequestFactory} actorRequestFactory
  852. * @constructor
  853. */
  854. constructor(actorRequestFactory) {
  855. this._requestFactory = actorRequestFactory;
  856. }
  857. /**
  858. * Creates a new builder of `Topic` instances of the provided type.
  859. *
  860. * @param {!Class<Message>} entity a Protobuf type of the subscription target entities
  861. * @return {TopicBuilder}
  862. */
  863. select(entity) {
  864. return new TopicBuilder(entity, this);
  865. }
  866. /**
  867. * Creates a `Topic` for the specified `Target`.
  868. *
  869. * @param {!Target} forTarget a `Target` to create a topic for
  870. * @param {?FieldMask} withMask a mask specifying fields to be returned
  871. * @return {Topic} the instance of `Topic`
  872. */
  873. compose({forTarget: target, withMask: fieldMask}) {
  874. const id = TopicFactory._generateId();
  875. const topic = new Topic();
  876. topic.setId(id);
  877. topic.setContext(this._requestFactory._actorContext());
  878. topic.setTarget(target);
  879. topic.setFieldMask(fieldMask);
  880. return topic;
  881. }
  882. /**
  883. * @return {TopicId} a newly created topic ID
  884. * @private
  885. */
  886. static _generateId() {
  887. const topicId = new TopicId();
  888. topicId.setValue(`t-${newUuid()}`);
  889. return topicId;
  890. }
  891. }
  892. /**
  893. * A provider of the actor that is used to associate requests to the backend
  894. * with an application user.
  895. */
  896. export class ActorProvider {
  897. /**
  898. * @param {?UserId} actor an optional actor to be used for identifying requests to the backend;
  899. * if not specified, the anonymous actor is used
  900. */
  901. constructor(actor) {
  902. this.update(actor);
  903. }
  904. /**
  905. * Updates the actor ID value if it is different from the current, sets the
  906. * anonymous actor value if actor ID not specified or `null`.
  907. *
  908. * @param {?UserId} actorId
  909. */
  910. update(actorId) {
  911. if (typeof actorId === 'undefined' || actorId === null) {
  912. this._actor = ActorProvider.ANONYMOUS;
  913. } else {
  914. ActorProvider._ensureUserId(actorId);
  915. if (!Message.equals(this._actor, actorId)) {
  916. this._actor = actorId;
  917. }
  918. }
  919. }
  920. /**
  921. * @return {UserId} the current actor value
  922. */
  923. get() {
  924. return this._actor;
  925. }
  926. /**
  927. * Sets the anonymous actor value.
  928. */
  929. clear() {
  930. this._actor = ActorProvider.ANONYMOUS;
  931. }
  932. /**
  933. * Ensures if the object extends {@link UserId}.
  934. *
  935. * The implementation doesn't use `instanceof` check and check on prototypes
  936. * since they may fail if different versions of the file are used at the same time
  937. * (e.g. bundled and the original one).
  938. *
  939. * @param object the object to check
  940. */
  941. static _ensureUserId(object) {
  942. if (!(isProtobufMessage(object) && typeof object.getValue === 'function')) {
  943. throw new Error('The `spine.core.UserId` type was expected by `ActorProvider`.');
  944. }
  945. }
  946. }
  947. /**
  948. * The anonymous backend actor.
  949. *
  950. * It is needed for requests to the backend when the particular user is undefined.
  951. *
  952. * @type UserId
  953. */
  954. ActorProvider.ANONYMOUS = function () {
  955. const actor = new UserId();
  956. actor.setValue('ANONYMOUS');
  957. return actor;
  958. }();
  959. /**
  960. * A factory for the various requests fired from the client-side by an actor.
  961. */
  962. export class ActorRequestFactory {
  963. /**
  964. * Creates a new instance of ActorRequestFactory for the given actor.
  965. *
  966. * @param {!ActorProvider} actorProvider a provider of an actor
  967. * @param {?TenantProvider} tenantProvider a provider of the current tenant, if omitted, the
  968. * application is considered single-tenant
  969. */
  970. constructor(actorProvider, tenantProvider) {
  971. this._actorProvider = actorProvider;
  972. this._tenantProvider = tenantProvider;
  973. }
  974. /**
  975. * Creates a new `ActorRequestFactory` based on the passed options.
  976. *
  977. * @param {!ClientOptions} options the client initialization options
  978. * @return {ActorRequestFactory} a new `ActorRequestFactory` instance
  979. */
  980. static create(options) {
  981. if (!options) {
  982. throw new Error('Client options are not defined.')
  983. }
  984. const actorProvider = options.actorProvider;
  985. if (!actorProvider) {
  986. throw new Error('The actor provider should be set in the client options in order to ' +
  987. 'construct an `ActorRequestFactory`.');
  988. }
  989. return new ActorRequestFactory(actorProvider, options.tenantProvider);
  990. }
  991. /**
  992. * Creates a new query factory for building various queries based on configuration of this
  993. * `ActorRequestFactory` instance.
  994. *
  995. * @return {QueryFactory}
  996. */
  997. query() {
  998. return new QueryFactory(this);
  999. }
  1000. /**
  1001. * Creates a new command factory for building various commands based on configuration of this
  1002. * `ActorRequestFactory` instance.
  1003. *
  1004. * @return {CommandFactory}
  1005. */
  1006. command() {
  1007. return new CommandFactory(this);
  1008. }
  1009. /**
  1010. * Creates a new topic factory for building subscription topics based on configuration of this
  1011. * `ActorRequestFactory` instance.
  1012. *
  1013. * @return {TopicFactory}
  1014. */
  1015. topic() {
  1016. return new TopicFactory(this);
  1017. }
  1018. _actorContext() {
  1019. const result = new ActorContext();
  1020. if (this._tenantProvider) {
  1021. result.setTenantId(this._tenantProvider.tenantId());
  1022. }
  1023. result.setActor(this._actorProvider.get());
  1024. const seconds = Math.round(new Date().getTime() / 1000);
  1025. const time = new Timestamp();
  1026. time.setSeconds(seconds);
  1027. result.setTimestamp(time);
  1028. result.setZoneOffset(ActorRequestFactory._zoneOffset());
  1029. return result;
  1030. }
  1031. /**
  1032. * @return {ZoneOffset}
  1033. * @protected
  1034. */
  1035. static _zoneOffset() {
  1036. const format = new Intl.DateTimeFormat();
  1037. const timeOptions = format.resolvedOptions();
  1038. const zoneId = new ZoneId();
  1039. zoneId.setValue(timeOptions.timeZone);
  1040. const zoneOffset = ActorRequestFactory._zoneOffsetSeconds();
  1041. const result = new ZoneOffset();
  1042. result.setAmountSeconds(zoneOffset);
  1043. return result;
  1044. }
  1045. /**
  1046. * @return {number}
  1047. * @private
  1048. */
  1049. static _zoneOffsetSeconds() {
  1050. return new Date().getTimezoneOffset() * 60;
  1051. }
  1052. }