@peabody-soft/mbs-model-ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +661 -0
- package/dist/Address.d.ts +33 -0
- package/dist/Address.js +97 -0
- package/dist/Attachment.d.ts +16 -0
- package/dist/Attachment.js +49 -0
- package/dist/AttachmentBaseUrl.d.ts +11 -0
- package/dist/AttachmentBaseUrl.js +41 -0
- package/dist/AuthTokens.d.ts +13 -0
- package/dist/AuthTokens.js +30 -0
- package/dist/Business.d.ts +44 -0
- package/dist/Business.js +87 -0
- package/dist/BusinessIdentifier.d.ts +8 -0
- package/dist/BusinessIdentifier.js +14 -0
- package/dist/BusinessLink.d.ts +21 -0
- package/dist/BusinessLink.js +61 -0
- package/dist/BusinessProfile.d.ts +11 -0
- package/dist/BusinessProfile.js +27 -0
- package/dist/CartEditor.d.ts +59 -0
- package/dist/CartEditor.js +324 -0
- package/dist/Catalog.d.ts +28 -0
- package/dist/Catalog.js +115 -0
- package/dist/Constants.d.ts +10 -0
- package/dist/Constants.js +43 -0
- package/dist/ContainerNames.d.ts +2 -0
- package/dist/ContainerNames.js +21 -0
- package/dist/Database.d.ts +15 -0
- package/dist/Database.js +1 -0
- package/dist/Entity.d.ts +27 -0
- package/dist/Entity.js +69 -0
- package/dist/Enums.d.ts +16 -0
- package/dist/Enums.js +18 -0
- package/dist/Exceptions.d.ts +4 -0
- package/dist/Exceptions.js +8 -0
- package/dist/GeneralSettings.d.ts +46 -0
- package/dist/GeneralSettings.js +99 -0
- package/dist/HttpStatusCodes.d.ts +2 -0
- package/dist/HttpStatusCodes.js +34 -0
- package/dist/Ids.d.ts +17 -0
- package/dist/Ids.js +78 -0
- package/dist/Item.d.ts +12 -0
- package/dist/Item.js +30 -0
- package/dist/LatLong.d.ts +7 -0
- package/dist/LatLong.js +17 -0
- package/dist/LiveData.d.ts +30 -0
- package/dist/LiveData.js +76 -0
- package/dist/Measures.d.ts +20 -0
- package/dist/Measures.js +34 -0
- package/dist/Message.d.ts +16 -0
- package/dist/Message.js +32 -0
- package/dist/MockDatabase.d.ts +19 -0
- package/dist/MockDatabase.js +73 -0
- package/dist/Model.d.ts +63 -0
- package/dist/Model.js +629 -0
- package/dist/Order.d.ts +31 -0
- package/dist/Order.js +70 -0
- package/dist/Package.d.ts +25 -0
- package/dist/Package.js +52 -0
- package/dist/PackageIdentifier.d.ts +8 -0
- package/dist/PackageIdentifier.js +14 -0
- package/dist/PaymentMethodDetail.d.ts +8 -0
- package/dist/PaymentMethodDetail.js +17 -0
- package/dist/Photo.d.ts +11 -0
- package/dist/Photo.js +29 -0
- package/dist/Price.d.ts +17 -0
- package/dist/Price.js +52 -0
- package/dist/PriceList.d.ts +3 -0
- package/dist/PriceList.js +5 -0
- package/dist/Product.d.ts +33 -0
- package/dist/Product.js +84 -0
- package/dist/ProductCategory.d.ts +10 -0
- package/dist/ProductCategory.js +18 -0
- package/dist/Profile.d.ts +20 -0
- package/dist/Profile.js +55 -0
- package/dist/ProtoMap.d.ts +71 -0
- package/dist/ProtoMap.js +1126 -0
- package/dist/Routes.d.ts +7 -0
- package/dist/Routes.js +12 -0
- package/dist/Service.d.ts +16 -0
- package/dist/Service.js +1 -0
- package/dist/ServiceExceptions.d.ts +40 -0
- package/dist/ServiceExceptions.js +84 -0
- package/dist/ServiceProxy.d.ts +25 -0
- package/dist/ServiceProxy.js +117 -0
- package/dist/ShoppingList.d.ts +64 -0
- package/dist/ShoppingList.js +188 -0
- package/dist/SyncCache.d.ts +28 -0
- package/dist/SyncCache.js +137 -0
- package/dist/SyncKeys.d.ts +5 -0
- package/dist/SyncKeys.js +41 -0
- package/dist/SyncState.d.ts +8 -0
- package/dist/SyncState.js +30 -0
- package/dist/Tax.d.ts +10 -0
- package/dist/Tax.js +30 -0
- package/dist/UserProfile.d.ts +16 -0
- package/dist/UserProfile.js +40 -0
- package/dist/WorkingDays.d.ts +33 -0
- package/dist/WorkingDays.js +121 -0
- package/dist/generated/Address.d.ts +77 -0
- package/dist/generated/Address.js +110 -0
- package/dist/generated/AddressList.d.ts +39 -0
- package/dist/generated/AddressList.js +60 -0
- package/dist/generated/AddressType.d.ts +15 -0
- package/dist/generated/AddressType.js +20 -0
- package/dist/generated/Attachment.d.ts +66 -0
- package/dist/generated/Attachment.js +89 -0
- package/dist/generated/AttachmentType.d.ts +154 -0
- package/dist/generated/AttachmentType.js +159 -0
- package/dist/generated/AuthTokens.d.ts +56 -0
- package/dist/generated/AuthTokens.js +74 -0
- package/dist/generated/AuthenticationMethod.d.ts +24 -0
- package/dist/generated/AuthenticationMethod.js +29 -0
- package/dist/generated/Business.d.ts +54 -0
- package/dist/generated/Business.js +75 -0
- package/dist/generated/BusinessIdentifier.d.ts +36 -0
- package/dist/generated/BusinessIdentifier.js +60 -0
- package/dist/generated/BusinessIdentifierType.d.ts +49 -0
- package/dist/generated/BusinessIdentifierType.js +54 -0
- package/dist/generated/BusinessLink.d.ts +76 -0
- package/dist/generated/BusinessLink.js +99 -0
- package/dist/generated/BusinessSyncInRequest.d.ts +42 -0
- package/dist/generated/BusinessSyncInRequest.js +60 -0
- package/dist/generated/BusinessSyncInResponse.d.ts +42 -0
- package/dist/generated/BusinessSyncInResponse.js +60 -0
- package/dist/generated/BusinessSyncOutRequest.d.ts +82 -0
- package/dist/generated/BusinessSyncOutRequest.js +107 -0
- package/dist/generated/BusinessSyncOutResponse.d.ts +106 -0
- package/dist/generated/BusinessSyncOutResponse.js +139 -0
- package/dist/generated/Configuration.d.ts +48 -0
- package/dist/generated/Configuration.js +63 -0
- package/dist/generated/Constants.d.ts +474 -0
- package/dist/generated/Constants.js +479 -0
- package/dist/generated/CreateUserRequest.d.ts +37 -0
- package/dist/generated/CreateUserRequest.js +59 -0
- package/dist/generated/DimensionUnit.d.ts +28 -0
- package/dist/generated/DimensionUnit.js +33 -0
- package/dist/generated/Dimensions.d.ts +41 -0
- package/dist/generated/Dimensions.js +76 -0
- package/dist/generated/Entity.d.ts +62 -0
- package/dist/generated/Entity.js +83 -0
- package/dist/generated/FulfillmentMode.d.ts +32 -0
- package/dist/generated/FulfillmentMode.js +37 -0
- package/dist/generated/GeneralSettings.d.ts +141 -0
- package/dist/generated/GeneralSettings.js +186 -0
- package/dist/generated/Item.d.ts +54 -0
- package/dist/generated/Item.js +82 -0
- package/dist/generated/LatLong.d.ts +36 -0
- package/dist/generated/LatLong.js +59 -0
- package/dist/generated/Message.d.ts +80 -0
- package/dist/generated/Message.js +114 -0
- package/dist/generated/MessageType.d.ts +34 -0
- package/dist/generated/MessageType.js +39 -0
- package/dist/generated/OrderStatus.d.ts +50 -0
- package/dist/generated/OrderStatus.js +55 -0
- package/dist/generated/Package.d.ts +63 -0
- package/dist/generated/Package.js +92 -0
- package/dist/generated/PackageIdentifier.d.ts +36 -0
- package/dist/generated/PackageIdentifier.js +60 -0
- package/dist/generated/PackageIdentifierType.d.ts +49 -0
- package/dist/generated/PackageIdentifierType.js +54 -0
- package/dist/generated/PartyPreferencesPayload.d.ts +41 -0
- package/dist/generated/PartyPreferencesPayload.js +60 -0
- package/dist/generated/PaymentMethod.d.ts +20 -0
- package/dist/generated/PaymentMethod.js +25 -0
- package/dist/generated/PaymentMethodDetail.d.ts +37 -0
- package/dist/generated/PaymentMethodDetail.js +59 -0
- package/dist/generated/Price.d.ts +52 -0
- package/dist/generated/Price.js +75 -0
- package/dist/generated/PriceList.d.ts +34 -0
- package/dist/generated/PriceList.js +51 -0
- package/dist/generated/Product.d.ts +77 -0
- package/dist/generated/Product.js +106 -0
- package/dist/generated/ProductCategory.d.ts +32 -0
- package/dist/generated/ProductCategory.js +51 -0
- package/dist/generated/ProductType.d.ts +42 -0
- package/dist/generated/ProductType.js +47 -0
- package/dist/generated/ProfileSyncOutRequest.d.ts +51 -0
- package/dist/generated/ProfileSyncOutRequest.js +75 -0
- package/dist/generated/ProfileSyncOutResponse.d.ts +56 -0
- package/dist/generated/ProfileSyncOutResponse.js +77 -0
- package/dist/generated/ReservedPackage.d.ts +34 -0
- package/dist/generated/ReservedPackage.js +39 -0
- package/dist/generated/Role.d.ts +71 -0
- package/dist/generated/Role.js +76 -0
- package/dist/generated/SalesOrderPayload.d.ts +85 -0
- package/dist/generated/SalesOrderPayload.js +123 -0
- package/dist/generated/ShoppingList.d.ts +99 -0
- package/dist/generated/ShoppingList.js +132 -0
- package/dist/generated/ShoppingListItem.d.ts +102 -0
- package/dist/generated/ShoppingListItem.js +139 -0
- package/dist/generated/TaskStatus.d.ts +20 -0
- package/dist/generated/TaskStatus.js +25 -0
- package/dist/generated/Tax.d.ts +37 -0
- package/dist/generated/Tax.js +60 -0
- package/dist/generated/TaxType.d.ts +25 -0
- package/dist/generated/TaxType.js +30 -0
- package/dist/generated/UrlAndToken.d.ts +54 -0
- package/dist/generated/UrlAndToken.js +67 -0
- package/dist/generated/User.d.ts +63 -0
- package/dist/generated/User.js +89 -0
- package/dist/generated/Weight.d.ts +33 -0
- package/dist/generated/Weight.js +60 -0
- package/dist/generated/WeightUnit.d.ts +28 -0
- package/dist/generated/WeightUnit.js +33 -0
- package/dist/generated/WorkingDays.d.ts +42 -0
- package/dist/generated/WorkingDays.js +68 -0
- package/dist/generated/WorkingHours.d.ts +33 -0
- package/dist/generated/WorkingHours.js +52 -0
- package/dist/generated/WorkingHoursRange.d.ts +42 -0
- package/dist/generated/WorkingHoursRange.js +59 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +60 -0
- package/package.json +55 -0
package/dist/ProtoMap.js
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import { Address as proto_Address } from "./generated/Address";
|
|
2
|
+
import { AddressList as proto_AddressList } from "./generated/AddressList";
|
|
3
|
+
import { Attachment as proto_Attachment } from "./generated/Attachment";
|
|
4
|
+
import { AuthTokens as proto_AuthTokens } from "./generated/AuthTokens";
|
|
5
|
+
import { Business as proto_Business } from "./generated/Business";
|
|
6
|
+
import { BusinessLink as proto_BusinessLink } from "./generated/BusinessLink";
|
|
7
|
+
import { BusinessSyncInRequest as proto_BusinessSyncInRequest } from "./generated/BusinessSyncInRequest";
|
|
8
|
+
import { BusinessSyncOutRequest as proto_BusinessSyncOutRequest } from "./generated/BusinessSyncOutRequest";
|
|
9
|
+
import { BusinessSyncOutResponse as proto_BusinessSyncOutResponse } from "./generated/BusinessSyncOutResponse";
|
|
10
|
+
import { Configuration as proto_Configuration } from "./generated/Configuration";
|
|
11
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
12
|
+
import { Entity as proto_Entity } from "./generated/Entity";
|
|
13
|
+
import { Message as proto_Message } from "./generated/Message";
|
|
14
|
+
import { MessageType as proto_MessageType } from "./generated/MessageType";
|
|
15
|
+
import { OrderStatus as proto_OrderStatus } from "./generated/OrderStatus";
|
|
16
|
+
import { Package as proto_Package } from "./generated/Package";
|
|
17
|
+
import { PartyPreferencesPayload as proto_PartyPreferencesPayload } from "./generated/PartyPreferencesPayload";
|
|
18
|
+
import { Price as proto_Price } from "./generated/Price";
|
|
19
|
+
import { PriceList as proto_PriceList } from "./generated/PriceList";
|
|
20
|
+
import { Product as proto_Product } from "./generated/Product";
|
|
21
|
+
import { ProductCategory as proto_ProductCategory } from "./generated/ProductCategory";
|
|
22
|
+
import { ProfileSyncOutRequest as proto_ProfileSyncOutRequest } from "./generated/ProfileSyncOutRequest";
|
|
23
|
+
import { ProfileSyncOutResponse as proto_ProfileSyncOutResponse } from "./generated/ProfileSyncOutResponse";
|
|
24
|
+
import { SalesOrderPayload as proto_SalesOrderPayload } from "./generated/SalesOrderPayload";
|
|
25
|
+
import { ShoppingList as proto_ShoppingList } from "./generated/ShoppingList";
|
|
26
|
+
import { ShoppingListItem as proto_ShoppingListItem } from "./generated/ShoppingListItem";
|
|
27
|
+
import { TaskStatus as proto_TaskStatus } from "./generated/TaskStatus";
|
|
28
|
+
import { Tax as proto_Tax } from "./generated/Tax";
|
|
29
|
+
import { UrlAndToken as proto_UrlAndToken } from "./generated/UrlAndToken";
|
|
30
|
+
import { User as proto_User } from "./generated/User";
|
|
31
|
+
import Address, { AddressList } from "./Address";
|
|
32
|
+
import Attachment from "./Attachment";
|
|
33
|
+
import AttachmentBaseUrl from "./AttachmentBaseUrl";
|
|
34
|
+
import AuthTokens from "./AuthTokens";
|
|
35
|
+
import Business from "./Business";
|
|
36
|
+
import BusinessIdentifier from "./BusinessIdentifier";
|
|
37
|
+
import BusinessLink from "./BusinessLink";
|
|
38
|
+
import BusinessProfile from "./BusinessProfile";
|
|
39
|
+
import Catalog from "./Catalog";
|
|
40
|
+
import { MERABILLS_ONLINE_PRICE_LIST_ID, amountToScaledValue, quantityToScaledValue, taxRateToScaledValue, } from "./Constants";
|
|
41
|
+
import { EntityHeader, UpdatableEntity } from "./Entity";
|
|
42
|
+
import { InvalidArgumentException } from "./Exceptions";
|
|
43
|
+
import GeneralSettings from "./GeneralSettings";
|
|
44
|
+
import LatLong from "./LatLong";
|
|
45
|
+
import Ids from "./Ids";
|
|
46
|
+
import { Dimensions, Weight } from "./Measures";
|
|
47
|
+
import Order from "./Order";
|
|
48
|
+
import Package from "./Package";
|
|
49
|
+
import PackageIdentifier from "./PackageIdentifier";
|
|
50
|
+
import PaymentMethodDetail from "./PaymentMethodDetail";
|
|
51
|
+
import Photo from "./Photo";
|
|
52
|
+
import Price from "./Price";
|
|
53
|
+
import PriceList from "./PriceList";
|
|
54
|
+
import Profile from "./Profile";
|
|
55
|
+
import Product from "./Product";
|
|
56
|
+
import ProductCategory from "./ProductCategory";
|
|
57
|
+
import ShoppingList, { ShoppingListItem } from "./ShoppingList";
|
|
58
|
+
import * as SyncKeys from "./SyncKeys";
|
|
59
|
+
import SyncState from "./SyncState";
|
|
60
|
+
import Tax from "./Tax";
|
|
61
|
+
import UserProfile from "./UserProfile";
|
|
62
|
+
import WorkingDays, { WorkingHours, WorkingHoursRange } from "./WorkingDays";
|
|
63
|
+
// #region A response and the blobs it is cached as
|
|
64
|
+
// Every entity of a response, in declaration order (parents before children). A tombstone is included, flagged.
|
|
65
|
+
export function getBusinessResponseEntities(response) {
|
|
66
|
+
const result = [];
|
|
67
|
+
if (response.business !== undefined)
|
|
68
|
+
result.push(toWireEntity(response.business.entity, proto_Business.toBinary(response.business)));
|
|
69
|
+
for (const attachment of response.attachments)
|
|
70
|
+
result.push(toWireEntity(attachment.entity, proto_Attachment.toBinary(attachment)));
|
|
71
|
+
for (const configuration of response.configurations)
|
|
72
|
+
result.push(toWireEntity(configuration.entity, proto_Configuration.toBinary(configuration)));
|
|
73
|
+
for (const message of response.messages)
|
|
74
|
+
result.push(toWireEntity(message.entity, proto_Message.toBinary(message)));
|
|
75
|
+
for (const productCategory of response.productCategories)
|
|
76
|
+
result.push(toItemWireEntity(productCategory.item, proto_ProductCategory.toBinary(productCategory)));
|
|
77
|
+
for (const product of response.products)
|
|
78
|
+
result.push(toItemWireEntity(product.item, proto_Product.toBinary(product)));
|
|
79
|
+
for (const pkg of response.packages)
|
|
80
|
+
result.push(toItemWireEntity(pkg.item, proto_Package.toBinary(pkg)));
|
|
81
|
+
for (const priceList of response.priceLists)
|
|
82
|
+
result.push(toItemWireEntity(priceList.item, proto_PriceList.toBinary(priceList)));
|
|
83
|
+
for (const price of response.prices)
|
|
84
|
+
result.push(toItemWireEntity(price.item, proto_Price.toBinary(price)));
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
// Every entity of a profile sync out response: the caller, their links, and the businesses those links name.
|
|
88
|
+
// The three have separate cursors, so unlike a business response the order between the arrays does not matter.
|
|
89
|
+
export function getProfileResponseEntities(response) {
|
|
90
|
+
const result = [];
|
|
91
|
+
if (response.user !== undefined)
|
|
92
|
+
result.push(toWireEntity(response.user.entity, proto_User.toBinary(response.user)));
|
|
93
|
+
for (const businessLink of response.businessLinks)
|
|
94
|
+
result.push(toWireEntity(businessLink.entity, proto_BusinessLink.toBinary(businessLink)));
|
|
95
|
+
for (const business of response.businesses)
|
|
96
|
+
result.push(toWireEntity(business.entity, proto_Business.toBinary(business)));
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// What a business response is cached as: one blob per live entity, the ids its tombstones named,
|
|
100
|
+
// and the attachment base url in a blob of its own, since it belongs to the business rather than to
|
|
101
|
+
// any entity of it.
|
|
102
|
+
export function businessResponseToCacheWrite(response) {
|
|
103
|
+
const write = toCacheWrite(getBusinessResponseEntities(response));
|
|
104
|
+
// Its eTag is its expiry, which is exactly its cursor: a client sends back the expiry it holds and is sent a later url only when one exists
|
|
105
|
+
const attachmentBaseUrl = response.attachmentBaseUrl;
|
|
106
|
+
if (attachmentBaseUrl !== undefined)
|
|
107
|
+
write.blobs.push({
|
|
108
|
+
id: Ids.ATTACHMENT_BASE_URL_BLOB_ID,
|
|
109
|
+
eTag: attachmentBaseUrl.expiry,
|
|
110
|
+
bytes: proto_UrlAndToken.toBinary(attachmentBaseUrl),
|
|
111
|
+
});
|
|
112
|
+
return write;
|
|
113
|
+
}
|
|
114
|
+
// The same for a profile response, which carries nothing but entities.
|
|
115
|
+
export function profileResponseToCacheWrite(response) {
|
|
116
|
+
return toCacheWrite(getProfileResponseEntities(response));
|
|
117
|
+
}
|
|
118
|
+
// The expiry of the attachment base url a response carried, and zero when it carried none, which is exactly what the next request sends back.
|
|
119
|
+
// Here rather than at a call site, because reading a field of a response is this file's job.
|
|
120
|
+
export function getBusinessResponseAttachmentBaseUrlExpiry(response) {
|
|
121
|
+
return response?.attachmentBaseUrl?.expiry ?? 0n;
|
|
122
|
+
}
|
|
123
|
+
// One order as its own blob, for a caller holding a message rather than a whole response. The payload is the only thing that says which a message is, so one carrying the other is refused here rather than stored where a read would never find it.
|
|
124
|
+
export function orderToBlob(order) {
|
|
125
|
+
if (order.payload.oneofKind !== "salesOrder")
|
|
126
|
+
throw new InvalidArgumentException("That message carries no sales order, so it is not an order");
|
|
127
|
+
return messageToBlob(order);
|
|
128
|
+
}
|
|
129
|
+
export function cartToBlob(cart) {
|
|
130
|
+
if (cart.payload.oneofKind !== "partyPreferences")
|
|
131
|
+
throw new InvalidArgumentException("That message carries no party preferences, so it is not a cart");
|
|
132
|
+
return messageToBlob(cart);
|
|
133
|
+
}
|
|
134
|
+
// The response the blobs of a business container make up: everything cached for that business, in the shape the call that brought it returned, and null when they hold nothing of it yet.
|
|
135
|
+
// The entity type in each blob id sorts them, so no stored type tag is needed; one this library does not know is skipped rather than refused, so a newer server cannot break an older client.
|
|
136
|
+
export function businessResponseFromBlobs(blobs) {
|
|
137
|
+
const response = proto_BusinessSyncOutResponse.create({});
|
|
138
|
+
let hasContent = false;
|
|
139
|
+
for (const blob of blobs) {
|
|
140
|
+
if (blob.id === Ids.ATTACHMENT_BASE_URL_BLOB_ID) {
|
|
141
|
+
response.attachmentBaseUrl = proto_UrlAndToken.fromBinary(blob.bytes);
|
|
142
|
+
hasContent = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (blob.id < 0n)
|
|
146
|
+
continue; // One of the other fixed blobs, not an entity
|
|
147
|
+
switch (Ids.getEntityTypeId(blob.id)) {
|
|
148
|
+
case proto_Constants.BUSINESS_ENTITY_TYPE_ID:
|
|
149
|
+
response.business = proto_Business.fromBinary(blob.bytes);
|
|
150
|
+
break;
|
|
151
|
+
case proto_Constants.ATTACHMENT_ENTITY_TYPE_ID:
|
|
152
|
+
response.attachments.push(proto_Attachment.fromBinary(blob.bytes));
|
|
153
|
+
break;
|
|
154
|
+
case proto_Constants.CONFIGURATION_ENTITY_TYPE_ID:
|
|
155
|
+
response.configurations.push(proto_Configuration.fromBinary(blob.bytes));
|
|
156
|
+
break;
|
|
157
|
+
case proto_Constants.MESSAGE_ENTITY_TYPE_ID:
|
|
158
|
+
response.messages.push(proto_Message.fromBinary(blob.bytes));
|
|
159
|
+
break;
|
|
160
|
+
case proto_Constants.PRODUCT_CATEGORY_ENTITY_TYPE_ID:
|
|
161
|
+
response.productCategories.push(proto_ProductCategory.fromBinary(blob.bytes));
|
|
162
|
+
break;
|
|
163
|
+
case proto_Constants.PRODUCT_ENTITY_TYPE_ID:
|
|
164
|
+
response.products.push(proto_Product.fromBinary(blob.bytes));
|
|
165
|
+
break;
|
|
166
|
+
case proto_Constants.PACKAGE_ENTITY_TYPE_ID:
|
|
167
|
+
response.packages.push(proto_Package.fromBinary(blob.bytes));
|
|
168
|
+
break;
|
|
169
|
+
case proto_Constants.PRICE_LIST_ENTITY_TYPE_ID:
|
|
170
|
+
response.priceLists.push(proto_PriceList.fromBinary(blob.bytes));
|
|
171
|
+
break;
|
|
172
|
+
case proto_Constants.PRICE_ENTITY_TYPE_ID:
|
|
173
|
+
response.prices.push(proto_Price.fromBinary(blob.bytes));
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
continue; // An entity type this library does not understand
|
|
177
|
+
}
|
|
178
|
+
hasContent = true;
|
|
179
|
+
}
|
|
180
|
+
return hasContent ? response : null;
|
|
181
|
+
}
|
|
182
|
+
// The same for a profile container, whose three entity types are siblings rather than a hierarchy.
|
|
183
|
+
export function profileResponseFromBlobs(blobs) {
|
|
184
|
+
const response = proto_ProfileSyncOutResponse.create({});
|
|
185
|
+
let hasContent = false;
|
|
186
|
+
for (const blob of blobs) {
|
|
187
|
+
if (blob.id < 0n)
|
|
188
|
+
continue; // A fixed blob, not an entity
|
|
189
|
+
switch (Ids.getEntityTypeId(blob.id)) {
|
|
190
|
+
case proto_Constants.USER_ENTITY_TYPE_ID:
|
|
191
|
+
response.user = proto_User.fromBinary(blob.bytes);
|
|
192
|
+
break;
|
|
193
|
+
case proto_Constants.BUSINESS_LINK_ENTITY_TYPE_ID:
|
|
194
|
+
response.businessLinks.push(proto_BusinessLink.fromBinary(blob.bytes));
|
|
195
|
+
break;
|
|
196
|
+
case proto_Constants.BUSINESS_ENTITY_TYPE_ID:
|
|
197
|
+
response.businesses.push(proto_Business.fromBinary(blob.bytes));
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
continue; // An entity type this library does not understand
|
|
201
|
+
}
|
|
202
|
+
hasContent = true;
|
|
203
|
+
}
|
|
204
|
+
return hasContent ? response : null;
|
|
205
|
+
}
|
|
206
|
+
// The orders a container holds. The payload a message carries is what says whether it is an order or a cart - Message.messageType is not read, because here the payload is typed.
|
|
207
|
+
export function orderMessagesFromBlobs(blobs) {
|
|
208
|
+
const result = [];
|
|
209
|
+
for (const message of messagesFromBlobs(blobs))
|
|
210
|
+
if (message.payload.oneofKind === "salesOrder")
|
|
211
|
+
result.push(message);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
// The cart a container holds, or null when the customer has not put anything in one. A customer has one cart per business, so two means two callers' messages are in one container.
|
|
215
|
+
// The most recently updated then wins for want of anything better: nothing in a message says which caller it was sent to - see THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
|
|
216
|
+
export function cartMessageFromBlobs(blobs) {
|
|
217
|
+
let result = null;
|
|
218
|
+
for (const message of messagesFromBlobs(blobs)) {
|
|
219
|
+
if (message.payload.oneofKind !== "partyPreferences")
|
|
220
|
+
continue;
|
|
221
|
+
if (result === null ||
|
|
222
|
+
getEntity(message.entity).updateNumber > getEntity(result.entity).updateNumber)
|
|
223
|
+
result = message;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
// #endregion
|
|
228
|
+
// #region Requests and credentials
|
|
229
|
+
// The request that asks for everything changed since these cursors.
|
|
230
|
+
// attachmentBaseUrlExpiry is not a cursor, just the expiry of the url already held, sent back unchanged (zero when none is held).
|
|
231
|
+
export function buildBusinessSyncOutRequest(businessId, syncInState, attachmentBaseUrlExpiry) {
|
|
232
|
+
return proto_BusinessSyncOutRequest.create({
|
|
233
|
+
businessId: businessId,
|
|
234
|
+
businessUpdateNumber: syncInState.getUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID),
|
|
235
|
+
attachmentUpdateNumber: syncInState.getUpdateNumber(proto_Constants.ATTACHMENT_ENTITY_TYPE_ID),
|
|
236
|
+
attachmentBaseUrlExpiry: attachmentBaseUrlExpiry,
|
|
237
|
+
configurationUpdateNumber: syncInState.getUpdateNumber(proto_Constants.CONFIGURATION_ENTITY_TYPE_ID),
|
|
238
|
+
messageUpdateNumber: syncInState.getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID),
|
|
239
|
+
itemUpdateNumber: syncInState.getUpdateNumber(proto_Constants.ITEM_ENTITY_TYPE_ID),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// The first request of a business sync in, with cursors raised from what is already cached rather
|
|
243
|
+
// than sent from zero - the same way nextBusinessSyncOutRequest raises them from a page just
|
|
244
|
+
// arrived, but from a response already held.
|
|
245
|
+
export function buildBusinessSyncOutRequestFromCache(businessId, cached) {
|
|
246
|
+
const cursors = new SyncState(SyncKeys.BUSINESS_SYNC_IN);
|
|
247
|
+
if (cached !== null)
|
|
248
|
+
for (const entity of getBusinessResponseEntities(cached))
|
|
249
|
+
cursors.raiseUpdateNumber(entity.entityTypeId, entity.updateNumber);
|
|
250
|
+
return buildBusinessSyncOutRequest(businessId, cursors, getBusinessResponseAttachmentBaseUrlExpiry(cached));
|
|
251
|
+
}
|
|
252
|
+
// The request that pushes back what this customer has changed. Messages are the only entity type a
|
|
253
|
+
// storefront may push, and one request is one batch: an order and the cart it emptied go together,
|
|
254
|
+
// so that a customer can never be left holding an order and a cart still full of the same items.
|
|
255
|
+
// messageUpdateNumber is the cursor of what this customer already holds cached, so the response
|
|
256
|
+
// brings back only what has changed since - this push's own messages, under the update numbers the
|
|
257
|
+
// server assigns them, and anything else of theirs a later sync out would also pick up.
|
|
258
|
+
export function buildBusinessSyncInRequest(messageUpdateNumber, messages) {
|
|
259
|
+
return proto_BusinessSyncInRequest.create({
|
|
260
|
+
messageUpdateNumber: messageUpdateNumber,
|
|
261
|
+
messages: [...messages],
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// The messages a sync in response carried - this push's own, now bearing the update numbers the
|
|
265
|
+
// server assigned them, and anything else of this customer's that changed since the cursor sent.
|
|
266
|
+
export function getSyncInResponseMessages(response) {
|
|
267
|
+
return response.messages;
|
|
268
|
+
}
|
|
269
|
+
// The same, for the profile of the caller - names no business, since a profile sync is not scoped
|
|
270
|
+
// to one and has no sync in.
|
|
271
|
+
// joinBusinessId is the exception, and it is not a scope but an instruction: make the caller a
|
|
272
|
+
// customer of that business, by entity id, while answering. Constants.ENTITY_ID_NULL asks for no
|
|
273
|
+
// join, which is what every ordinary read carries, and it is the default for that reason. A join
|
|
274
|
+
// belongs to one call and not to a read, so nextProfileSyncOutRequest leaves it off the pages that
|
|
275
|
+
// follow - see there.
|
|
276
|
+
export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(proto_Constants.ENTITY_ID_NULL)) {
|
|
277
|
+
return proto_ProfileSyncOutRequest.create({
|
|
278
|
+
userUpdateNumber: syncInState.getUpdateNumber(proto_Constants.USER_ENTITY_TYPE_ID),
|
|
279
|
+
businessUpdateNumber: syncInState.getUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID),
|
|
280
|
+
businessLinkUpdateNumber: syncInState.getUpdateNumber(proto_Constants.BUSINESS_LINK_ENTITY_TYPE_ID),
|
|
281
|
+
joinBusinessId: joinBusinessId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// The same, seeded from what the profile container already holds cached rather than from a fresh
|
|
285
|
+
// SyncState - see buildBusinessSyncOutRequestFromCache, which this mirrors.
|
|
286
|
+
export function buildProfileSyncOutRequestFromCache(cached, joinBusinessId = BigInt(proto_Constants.ENTITY_ID_NULL)) {
|
|
287
|
+
const cursors = new SyncState(SyncKeys.PROFILE_SYNC_IN);
|
|
288
|
+
if (cached !== null)
|
|
289
|
+
for (const entity of getProfileResponseEntities(cached))
|
|
290
|
+
cursors.raiseUpdateNumber(entity.entityTypeId, entity.updateNumber);
|
|
291
|
+
return buildProfileSyncOutRequest(cursors, joinBusinessId);
|
|
292
|
+
}
|
|
293
|
+
// The user a sign up asks the server to create, as CreateUserRequest.user. Built from the domain
|
|
294
|
+
// object rather than carrying one of its own: this user has no entity yet - the server assigns id,
|
|
295
|
+
// createdAt and updatedAt on creation - so none of EntityHeader goes out here.
|
|
296
|
+
export function buildUser(userProfile) {
|
|
297
|
+
return proto_User.create({
|
|
298
|
+
firstName: userProfile.firstName,
|
|
299
|
+
lastName: stringToProto(userProfile.lastName),
|
|
300
|
+
mobilePhoneNumber: userProfile.mobilePhoneNumber,
|
|
301
|
+
addresses: addressListToProto(userProfile.addresses),
|
|
302
|
+
preferredLanguageCode: stringToProto(userProfile.preferredLanguageCode),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// The user Service.updateUser asks the server to overwrite this customer's profile with - the same
|
|
306
|
+
// fields as buildUser, plus the one field that call matches the caller by: its own entity id.
|
|
307
|
+
// Nothing else of EntityHeader goes out, because nothing else of it is read on that call.
|
|
308
|
+
export function buildUpdateUser(userProfile) {
|
|
309
|
+
return proto_User.create({
|
|
310
|
+
...buildUser(userProfile),
|
|
311
|
+
entity: proto_Entity.create({ id: userProfile.id }),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// The two halves of what a join must be answered with, each asked separately so that a caller
|
|
315
|
+
// reporting a server that answered without one can say which was missing - see
|
|
316
|
+
// Model.requireJoinedBusiness. Both take the id rather than the request, because both of the
|
|
317
|
+
// requests that carry a joinBusinessId need them.
|
|
318
|
+
// The membership itself: a live BusinessLink naming that business. A tombstoned one is not a
|
|
319
|
+
// membership - see OBJECT MODEL - so it does not count, and neither does a link to any other
|
|
320
|
+
// business. Its linkToken is the only copy of that credential a profile response carries.
|
|
321
|
+
export function hasJoinedBusinessLink(joinBusinessId, response) {
|
|
322
|
+
if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
323
|
+
return false;
|
|
324
|
+
for (const businessLink of response.businessLinks)
|
|
325
|
+
if (businessLink.businessId === joinBusinessId &&
|
|
326
|
+
!isDeletedEntity(getEntity(businessLink.entity)))
|
|
327
|
+
return true;
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
// The business that link names, matched on Business.entity.id - which is what
|
|
331
|
+
// BusinessLink.businessId is, and not Business.businessId, the url friendly string. A caller needs
|
|
332
|
+
// it as well as the link, because the string is what every later request and container name for
|
|
333
|
+
// that business is built out of, and the link carries only the id.
|
|
334
|
+
export function hasJoinedBusiness(joinBusinessId, response) {
|
|
335
|
+
if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
336
|
+
return false;
|
|
337
|
+
for (const business of response.businesses)
|
|
338
|
+
if (getEntity(business.entity).id === joinBusinessId)
|
|
339
|
+
return true;
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// The credentials of one call, as the wire carries them. An absent token is left off the message
|
|
343
|
+
// rather than sent empty, because the protocol tells the two apart.
|
|
344
|
+
export function buildAuthTokens(authTokens) {
|
|
345
|
+
const result = proto_AuthTokens.create({
|
|
346
|
+
authenticationMethod: authTokens.authenticationMethod,
|
|
347
|
+
authenticationToken: authTokens.authenticationToken,
|
|
348
|
+
});
|
|
349
|
+
const userToken = authTokens.userToken;
|
|
350
|
+
if (userToken !== null)
|
|
351
|
+
result.userToken = userToken;
|
|
352
|
+
const linkToken = authTokens.linkToken;
|
|
353
|
+
if (linkToken !== null)
|
|
354
|
+
result.linkToken = linkToken;
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
// What a server issued while answering a call, in the terms this library holds credentials in
|
|
358
|
+
export function authTokensFromProto(authTokens) {
|
|
359
|
+
return new AuthTokens(authTokens.authenticationMethod, authTokens.authenticationToken, stringFromProto(authTokens.userToken), stringFromProto(authTokens.linkToken));
|
|
360
|
+
}
|
|
361
|
+
// The authorization header of a request, encoded. Lives here because this is the only file that
|
|
362
|
+
// writes a proto field; a transport puts these bytes in the header however it wants.
|
|
363
|
+
export function buildAuthTokensBytes(authTokens) {
|
|
364
|
+
return proto_AuthTokens.toBinary(authTokens);
|
|
365
|
+
}
|
|
366
|
+
// How a container keeps the credentials it earned: the two tokens this server issued, with a null eTag because a reissued token is not a later version of an earlier one and greater-wins would refuse it.
|
|
367
|
+
// The authentication token is left out however it was passed in: it belongs to the identity provider, so a stored copy could only be staler than the one held in memory and would outlive the sign out that should have ended it.
|
|
368
|
+
export function authTokensToBlob(authTokens) {
|
|
369
|
+
const stored = proto_AuthTokens.create({
|
|
370
|
+
authenticationMethod: authTokens.authenticationMethod,
|
|
371
|
+
});
|
|
372
|
+
const userToken = authTokens.userToken;
|
|
373
|
+
if (userToken !== null)
|
|
374
|
+
stored.userToken = userToken;
|
|
375
|
+
const linkToken = authTokens.linkToken;
|
|
376
|
+
if (linkToken !== null)
|
|
377
|
+
stored.linkToken = linkToken;
|
|
378
|
+
return {
|
|
379
|
+
id: Ids.AUTH_TOKENS_BLOB_ID,
|
|
380
|
+
eTag: null,
|
|
381
|
+
bytes: buildAuthTokensBytes(stored),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
export function authTokensFromBytes(bytes) {
|
|
385
|
+
return authTokensFromProto(proto_AuthTokens.fromBinary(bytes));
|
|
386
|
+
}
|
|
387
|
+
// #endregion
|
|
388
|
+
// #region Paging
|
|
389
|
+
// What the server issued while answering, which every response of this protocol carries in the same
|
|
390
|
+
// field - the two sync outs and a sync in alike.
|
|
391
|
+
export function getResponseAuthTokens(response) {
|
|
392
|
+
return response.authTokens;
|
|
393
|
+
}
|
|
394
|
+
export function refreshAuthTokens(authTokens, issued) {
|
|
395
|
+
if (authTokens === null || issued === undefined)
|
|
396
|
+
return authTokens;
|
|
397
|
+
const result = proto_AuthTokens.create(authTokens);
|
|
398
|
+
if (issued.userToken !== undefined)
|
|
399
|
+
result.userToken = issued.userToken;
|
|
400
|
+
if (issued.linkToken !== undefined)
|
|
401
|
+
result.linkToken = issued.linkToken;
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
// The request that asks for the page after this response, or null when this response was the last.
|
|
405
|
+
// A response carrying a full page has been cut short, so the cursors are raised from what arrived
|
|
406
|
+
// and the call is repeated. A full page that raised no cursor at all can only come from a server
|
|
407
|
+
// breaking the ascending order it promises; the loop is unbounded, so it stops rather than trust it.
|
|
408
|
+
export function nextBusinessSyncOutRequest(request, response) {
|
|
409
|
+
// Anything short of a full page is the server saying it has sent everything it had. Counted
|
|
410
|
+
// before anything is looked at, so the ordinary answer costs no more than reading nine lengths.
|
|
411
|
+
if (businessResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
412
|
+
return null;
|
|
413
|
+
const cursors = businessSyncOutCursors(request);
|
|
414
|
+
let haveCursorsRisen = false;
|
|
415
|
+
const raise = (entity) => {
|
|
416
|
+
if (raiseCursor(cursors, entity))
|
|
417
|
+
haveCursorsRisen = true;
|
|
418
|
+
};
|
|
419
|
+
if (response.business !== undefined)
|
|
420
|
+
raise(response.business.entity);
|
|
421
|
+
for (const attachment of response.attachments)
|
|
422
|
+
raise(attachment.entity);
|
|
423
|
+
for (const configuration of response.configurations)
|
|
424
|
+
raise(configuration.entity);
|
|
425
|
+
for (const message of response.messages)
|
|
426
|
+
raise(message.entity);
|
|
427
|
+
for (const productCategory of response.productCategories)
|
|
428
|
+
raise(getItem(productCategory.item).entity);
|
|
429
|
+
for (const product of response.products)
|
|
430
|
+
raise(getItem(product.item).entity);
|
|
431
|
+
for (const pkg of response.packages)
|
|
432
|
+
raise(getItem(pkg.item).entity);
|
|
433
|
+
for (const priceList of response.priceLists)
|
|
434
|
+
raise(getItem(priceList.item).entity);
|
|
435
|
+
for (const price of response.prices)
|
|
436
|
+
raise(getItem(price.item).entity);
|
|
437
|
+
if (!haveCursorsRisen)
|
|
438
|
+
return null;
|
|
439
|
+
// A page that renewed the url means a later one is now held; otherwise what was sent still stands
|
|
440
|
+
const renewedExpiry = getBusinessResponseAttachmentBaseUrlExpiry(response);
|
|
441
|
+
return buildBusinessSyncOutRequest(request.businessId, cursors, renewedExpiry === 0n ? request.attachmentBaseUrlExpiry : renewedExpiry);
|
|
442
|
+
}
|
|
443
|
+
// The same for a profile read. Any joinBusinessId the first request carried is deliberately left
|
|
444
|
+
// off the ones that follow: a join is done once, by the call that asked for it.
|
|
445
|
+
export function nextProfileSyncOutRequest(request, response) {
|
|
446
|
+
if (profileResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
447
|
+
return null;
|
|
448
|
+
const cursors = profileSyncOutCursors(request);
|
|
449
|
+
let haveCursorsRisen = false;
|
|
450
|
+
if (raiseCursor(cursors, response.user?.entity))
|
|
451
|
+
haveCursorsRisen = true;
|
|
452
|
+
for (const businessLink of response.businessLinks)
|
|
453
|
+
if (raiseCursor(cursors, businessLink.entity))
|
|
454
|
+
haveCursorsRisen = true;
|
|
455
|
+
for (const business of response.businesses)
|
|
456
|
+
if (raiseCursor(cursors, business.entity))
|
|
457
|
+
haveCursorsRisen = true;
|
|
458
|
+
// A full page that raised nothing can only come from a server breaking the ascending order it
|
|
459
|
+
// promises; the loop is unbounded, so it stops rather than trust it
|
|
460
|
+
if (!haveCursorsRisen)
|
|
461
|
+
return null;
|
|
462
|
+
return buildProfileSyncOutRequest(cursors);
|
|
463
|
+
}
|
|
464
|
+
// How many entities a response carried, without decoding or encoding any of them - a paging loop
|
|
465
|
+
// asks this of every page, and getBusinessResponseEntities answers the same question only by encoding each
|
|
466
|
+
// entity for the cache on the way past.
|
|
467
|
+
function businessResponseEntityCount(response) {
|
|
468
|
+
return ((response.business === undefined ? 0 : 1) +
|
|
469
|
+
response.attachments.length +
|
|
470
|
+
response.configurations.length +
|
|
471
|
+
response.messages.length +
|
|
472
|
+
response.productCategories.length +
|
|
473
|
+
response.products.length +
|
|
474
|
+
response.packages.length +
|
|
475
|
+
response.priceLists.length +
|
|
476
|
+
response.prices.length);
|
|
477
|
+
}
|
|
478
|
+
function profileResponseEntityCount(response) {
|
|
479
|
+
return ((response.user === undefined ? 0 : 1) +
|
|
480
|
+
response.businessLinks.length +
|
|
481
|
+
response.businesses.length);
|
|
482
|
+
}
|
|
483
|
+
// Raises the cursor one entity advances, and answers whether it moved. A tombstone counts exactly
|
|
484
|
+
// as a value does - skipping one would have the server send it again for ever.
|
|
485
|
+
function raiseCursor(cursors, entity) {
|
|
486
|
+
if (entity === undefined)
|
|
487
|
+
return false;
|
|
488
|
+
return cursors.raiseUpdateNumber(Ids.getEntityTypeId(entity.id), entity.updateNumber);
|
|
489
|
+
}
|
|
490
|
+
// The cursors a request is carrying, as a state that can be raised - the inverse of the two builders
|
|
491
|
+
// above, and how a paging loop carries what it has already been sent into the call after it.
|
|
492
|
+
function businessSyncOutCursors(request) {
|
|
493
|
+
const cursors = new SyncState(SyncKeys.BUSINESS_SYNC_IN);
|
|
494
|
+
cursors.raiseUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID, request.businessUpdateNumber);
|
|
495
|
+
cursors.raiseUpdateNumber(proto_Constants.ATTACHMENT_ENTITY_TYPE_ID, request.attachmentUpdateNumber);
|
|
496
|
+
cursors.raiseUpdateNumber(proto_Constants.CONFIGURATION_ENTITY_TYPE_ID, request.configurationUpdateNumber);
|
|
497
|
+
cursors.raiseUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID, request.messageUpdateNumber);
|
|
498
|
+
cursors.raiseUpdateNumber(proto_Constants.ITEM_ENTITY_TYPE_ID, request.itemUpdateNumber);
|
|
499
|
+
return cursors;
|
|
500
|
+
}
|
|
501
|
+
function profileSyncOutCursors(request) {
|
|
502
|
+
const cursors = new SyncState(SyncKeys.PROFILE_SYNC_IN);
|
|
503
|
+
cursors.raiseUpdateNumber(proto_Constants.USER_ENTITY_TYPE_ID, request.userUpdateNumber);
|
|
504
|
+
cursors.raiseUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID, request.businessUpdateNumber);
|
|
505
|
+
cursors.raiseUpdateNumber(proto_Constants.BUSINESS_LINK_ENTITY_TYPE_ID, request.businessLinkUpdateNumber);
|
|
506
|
+
return cursors;
|
|
507
|
+
}
|
|
508
|
+
// #endregion
|
|
509
|
+
// #region A response as an object graph
|
|
510
|
+
// The whole of one business as an object graph, regrouping the flat wire entities by owner id so each class can be constructed with its children.
|
|
511
|
+
// An entity whose parent is missing is dropped, since a paged read can hold a child whose parent has yet to arrive; null when the response carries no Business at all.
|
|
512
|
+
export function buildBusiness(response) {
|
|
513
|
+
const businessProto = response.business;
|
|
514
|
+
if (businessProto === undefined || isDeletedEntity(getEntity(businessProto.entity)))
|
|
515
|
+
return null;
|
|
516
|
+
const business = businessFromProto(businessProto);
|
|
517
|
+
const updatableBusiness = business;
|
|
518
|
+
// Resolved once here and handed to every Photo built below, since a photo takes the base url in
|
|
519
|
+
// effect when it is built rather than looking one up later - see the comment on Photo's constructor.
|
|
520
|
+
const attachmentBaseUrlProto = response.attachmentBaseUrl;
|
|
521
|
+
const attachmentBaseUrl = attachmentBaseUrlProto === undefined ? null : attachmentBaseUrlFromProto(attachmentBaseUrlProto);
|
|
522
|
+
updatableBusiness.attachmentBaseUrl = attachmentBaseUrl;
|
|
523
|
+
// Attachments, grouped by owner
|
|
524
|
+
const attachmentsByOwner = new Map();
|
|
525
|
+
for (const attachmentProto of response.attachments) {
|
|
526
|
+
if (isDeletedEntity(getEntity(attachmentProto.entity)))
|
|
527
|
+
continue;
|
|
528
|
+
const attachment = attachmentFromProto(attachmentProto);
|
|
529
|
+
const owned = attachmentsByOwner.get(attachment.owningEntityId);
|
|
530
|
+
if (owned === undefined)
|
|
531
|
+
attachmentsByOwner.set(attachment.owningEntityId, [attachment]);
|
|
532
|
+
else
|
|
533
|
+
owned.push(attachment);
|
|
534
|
+
}
|
|
535
|
+
// The settings, unwrapped from the one public configuration that carries them
|
|
536
|
+
for (const configuration of response.configurations) {
|
|
537
|
+
if (isDeletedEntity(getEntity(configuration.entity)))
|
|
538
|
+
continue;
|
|
539
|
+
if (configuration.data.oneofKind !== "generalSettings")
|
|
540
|
+
continue; // Not a configuration this library understands
|
|
541
|
+
updatableBusiness.settings = generalSettingsFromProto(configuration.data.generalSettings);
|
|
542
|
+
}
|
|
543
|
+
// The photos of the store itself
|
|
544
|
+
updatableBusiness.photos = takePhotos(attachmentsByOwner, business.id, attachmentBaseUrl);
|
|
545
|
+
// Prices, by the package each applies to. Only the MeraBills Online list is this storefront's -
|
|
546
|
+
// a business prices the same package in several lists, and a price from any other one is not
|
|
547
|
+
// what it sells at here, so it is passed over rather than allowed to win a package. Within that
|
|
548
|
+
// one list a package has one price; if a second arrives anyway, the first is kept.
|
|
549
|
+
const pricesByPackage = new Map();
|
|
550
|
+
for (const priceProto of response.prices) {
|
|
551
|
+
if (isDeletedItem(priceProto.item))
|
|
552
|
+
continue;
|
|
553
|
+
const price = priceFromProto(priceProto);
|
|
554
|
+
if (price.priceListId !== MERABILLS_ONLINE_PRICE_LIST_ID)
|
|
555
|
+
continue;
|
|
556
|
+
if (!pricesByPackage.has(price.packageId))
|
|
557
|
+
pricesByPackage.set(price.packageId, price);
|
|
558
|
+
}
|
|
559
|
+
// Packages, by the product each belongs to, built with their photos and their price. A package
|
|
560
|
+
// that is not buyable - disabled, unpriced, or priced with a price that is itself disabled - is
|
|
561
|
+
// left out here rather than handed to the UI unbuyable: a product-level filter does not, by
|
|
562
|
+
// itself, guarantee every package of an otherwise visible product is itself enabled.
|
|
563
|
+
const packagesByProduct = new Map();
|
|
564
|
+
for (const packageProto of response.packages) {
|
|
565
|
+
if (isDeletedItem(packageProto.item))
|
|
566
|
+
continue;
|
|
567
|
+
const packageId = getItemId(packageProto.item);
|
|
568
|
+
const packagePhotos = takePhotos(attachmentsByOwner, packageId, attachmentBaseUrl);
|
|
569
|
+
const pkg = packageFromProto(packageProto, packagePhotos, pricesByPackage.get(packageId) ?? null);
|
|
570
|
+
if (!pkg.isBuyable)
|
|
571
|
+
continue;
|
|
572
|
+
const price = pkg.price;
|
|
573
|
+
if (price !== null)
|
|
574
|
+
UpdatableEntity.setParent(price, pkg);
|
|
575
|
+
const siblings = packagesByProduct.get(pkg.productId);
|
|
576
|
+
if (siblings === undefined)
|
|
577
|
+
packagesByProduct.set(pkg.productId, [pkg]);
|
|
578
|
+
else
|
|
579
|
+
siblings.push(pkg);
|
|
580
|
+
}
|
|
581
|
+
// Products, built with their photos and their buyable packages, and parented to the business
|
|
582
|
+
// (not their category, which only groups them). A product that is not buyable - disabled, or
|
|
583
|
+
// left with no buyable package by the filtering above - is left out for the same reason.
|
|
584
|
+
const products = [];
|
|
585
|
+
for (const productProto of response.products) {
|
|
586
|
+
if (isDeletedItem(productProto.item))
|
|
587
|
+
continue;
|
|
588
|
+
const productId = getItemId(productProto.item);
|
|
589
|
+
const packages = packagesByProduct.get(productId) ?? [];
|
|
590
|
+
const productPhotos = takePhotos(attachmentsByOwner, productId, attachmentBaseUrl);
|
|
591
|
+
const product = productFromProto(productProto, productPhotos, Object.freeze([...packages]));
|
|
592
|
+
if (!product.isBuyable)
|
|
593
|
+
continue;
|
|
594
|
+
UpdatableEntity.setParent(product, business);
|
|
595
|
+
for (const pkg of packages)
|
|
596
|
+
UpdatableEntity.setParent(pkg, product);
|
|
597
|
+
products.push(product);
|
|
598
|
+
}
|
|
599
|
+
const productCategories = [];
|
|
600
|
+
for (const productCategoryProto of response.productCategories) {
|
|
601
|
+
if (isDeletedItem(productCategoryProto.item))
|
|
602
|
+
continue;
|
|
603
|
+
const productCategory = productCategoryFromProto(productCategoryProto);
|
|
604
|
+
UpdatableEntity.setParent(productCategory, business);
|
|
605
|
+
productCategories.push(productCategory);
|
|
606
|
+
}
|
|
607
|
+
// The one list a storefront sells from is MeraBills Online, named rather than taken to be
|
|
608
|
+
// whichever list arrived first: the same sync can carry others, and the catalog has to answer
|
|
609
|
+
// with the list the prices above came from or a cart would name one they did not.
|
|
610
|
+
let priceList = null;
|
|
611
|
+
for (const priceListProto of response.priceLists) {
|
|
612
|
+
if (isDeletedItem(priceListProto.item))
|
|
613
|
+
continue;
|
|
614
|
+
if (getItemId(priceListProto.item) !== MERABILLS_ONLINE_PRICE_LIST_ID)
|
|
615
|
+
continue;
|
|
616
|
+
priceList = priceListFromProto(priceListProto);
|
|
617
|
+
UpdatableEntity.setParent(priceList, business);
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
|
|
621
|
+
// The orders of this customer, and their cart. A message of any other payload type is dropped.
|
|
622
|
+
const orders = [];
|
|
623
|
+
let cart = null;
|
|
624
|
+
let cartUpdateNumber = 0n;
|
|
625
|
+
for (const message of response.messages) {
|
|
626
|
+
if (isDeletedEntity(getEntity(message.entity)))
|
|
627
|
+
continue;
|
|
628
|
+
switch (message.payload.oneofKind) {
|
|
629
|
+
case "salesOrder": {
|
|
630
|
+
orders.push(orderFromProto(message, message.payload.salesOrder));
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "partyPreferences": {
|
|
634
|
+
// One customer has one cart, so two means two callers' messages are in one container - see
|
|
635
|
+
// cartMessageFromBlobs, which breaks the tie the same way
|
|
636
|
+
const updateNumber = getEntity(message.entity).updateNumber;
|
|
637
|
+
if (cart !== null && updateNumber <= cartUpdateNumber)
|
|
638
|
+
break;
|
|
639
|
+
// An emptied cart is a message whose payload carries no shopping list, which is what
|
|
640
|
+
// placing an order leaves behind - so it answers null and not an empty list, the same as
|
|
641
|
+
// never having had a cart at all
|
|
642
|
+
cart = cartShoppingListFromProto(message.payload.partyPreferences);
|
|
643
|
+
cartUpdateNumber = updateNumber;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
// No default: MONEY_TRANSFER has no payload in this protocol, and OTHER is not ours
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (const order of orders)
|
|
650
|
+
UpdatableEntity.setParent(order, business);
|
|
651
|
+
updatableBusiness.orders = Object.freeze(orders);
|
|
652
|
+
updatableBusiness.cart = cart;
|
|
653
|
+
return business;
|
|
654
|
+
}
|
|
655
|
+
// One profile as an object graph: the caller, their links, and the businesses those links name.
|
|
656
|
+
// Returns null when the response carries none of the three.
|
|
657
|
+
export function buildProfile(profileId, response) {
|
|
658
|
+
let user = null;
|
|
659
|
+
const businessLinks = [];
|
|
660
|
+
const businesses = [];
|
|
661
|
+
const userProto = response.user;
|
|
662
|
+
if (userProto !== undefined && !isDeletedEntity(getEntity(userProto.entity)))
|
|
663
|
+
user = userProfileFromProto(userProto);
|
|
664
|
+
for (const businessLink of response.businessLinks) {
|
|
665
|
+
if (isDeletedEntity(getEntity(businessLink.entity)))
|
|
666
|
+
continue;
|
|
667
|
+
businessLinks.push(businessLinkFromProto(businessLink));
|
|
668
|
+
}
|
|
669
|
+
for (const business of response.businesses) {
|
|
670
|
+
if (isDeletedEntity(getEntity(business.entity)))
|
|
671
|
+
continue;
|
|
672
|
+
businesses.push(businessProfileFromProto(business));
|
|
673
|
+
}
|
|
674
|
+
if (user === null && businessLinks.length === 0 && businesses.length === 0)
|
|
675
|
+
return null;
|
|
676
|
+
return new Profile(profileId, user, businessLinks, businesses);
|
|
677
|
+
}
|
|
678
|
+
// One order on its own, for a caller holding a message rather than a whole response
|
|
679
|
+
export function buildOrder(message) {
|
|
680
|
+
if (message.payload.oneofKind !== "salesOrder")
|
|
681
|
+
return null;
|
|
682
|
+
return orderFromProto(message, message.payload.salesOrder);
|
|
683
|
+
}
|
|
684
|
+
// The cart one message holds, for a caller holding a message rather than a whole response. Null for
|
|
685
|
+
// a message that is not a cart, and null for a cart that has been emptied - see the party
|
|
686
|
+
// preferences case of buildBusiness.
|
|
687
|
+
export function buildCart(message) {
|
|
688
|
+
if (message.payload.oneofKind !== "partyPreferences")
|
|
689
|
+
return null;
|
|
690
|
+
return cartShoppingListFromProto(message.payload.partyPreferences);
|
|
691
|
+
}
|
|
692
|
+
// #endregion
|
|
693
|
+
// #region What a customer pushes
|
|
694
|
+
//
|
|
695
|
+
// The only direction of this file that encodes rather than decodes, and the only entity type a
|
|
696
|
+
// storefront may write: a Message, carrying either the cart or an order placed from it. Two rules
|
|
697
|
+
// hold for both, and they are what make a pushed message storable here as well as on the server.
|
|
698
|
+
// The first is that the id is the client's: a sync in answers with an update number and no id, so a
|
|
699
|
+
// message the server named could never be found here again. The second is that updatedAt is the
|
|
700
|
+
// client's too and the server keeps it unchanged - so the copy stored here carries the same eTag as
|
|
701
|
+
// the copy a later read would bring back, and neither can look stale to the other.
|
|
702
|
+
// The cart of this customer as a message to push. An existing message is amended rather than
|
|
703
|
+
// rebuilt, because its payload carries fields this protocol does not expose - the fulfillment mode
|
|
704
|
+
// the merchant app keeps there among them - and losing them would be a storefront overwriting
|
|
705
|
+
// merchant data it cannot even read.
|
|
706
|
+
// Its id is used in preference to the derived one for the same reason: that is where the server
|
|
707
|
+
// already holds this cart, and it is only worth deriving an id when there is no cart to amend.
|
|
708
|
+
// A list holding nothing is pushed as no list at all, the way Java serializeShoppingList answers
|
|
709
|
+
// null below one item - so emptying a cart and never having had one are one state on the wire, and
|
|
710
|
+
// placing an order is what puts a cart into it.
|
|
711
|
+
export function buildCartMessage(cartMessageId, existing, senderId, shoppingList, now) {
|
|
712
|
+
const existingEntity = existing === null ? null : getEntity(existing.entity);
|
|
713
|
+
const preferences = existing !== null && existing.payload.oneofKind === "partyPreferences"
|
|
714
|
+
? existing.payload.partyPreferences
|
|
715
|
+
: proto_PartyPreferencesPayload.create();
|
|
716
|
+
// Later than the copy it replaces, whatever this client's clock reads. A store keeps a blob only
|
|
717
|
+
// where its eTag is the greater, and that eTag is this field - so a client whose clock sits behind
|
|
718
|
+
// the time on the cart it is amending, which Constants.MAX_CLOCK_SKEW says is a thing that
|
|
719
|
+
// happens, would push a change the server accepted and then have its own store refuse it, leaving
|
|
720
|
+
// the two disagreeing about the cart the customer is looking at. One millisecond past the copy
|
|
721
|
+
// being replaced is enough, and the server keeps whatever is sent, so both sides still agree.
|
|
722
|
+
const updatedAt = existingEntity === null || existingEntity.updatedAt < now ? now : existingEntity.updatedAt + 1n;
|
|
723
|
+
return proto_Message.create({
|
|
724
|
+
entity: pushedEntity(existingEntity?.id ?? cartMessageId, existingEntity?.createdAt ?? updatedAt, updatedAt),
|
|
725
|
+
senderId: senderId,
|
|
726
|
+
messageType: proto_MessageType.PARTY_PREFERENCES,
|
|
727
|
+
status: proto_TaskStatus.NOT_YET_PROCESSED,
|
|
728
|
+
payload: {
|
|
729
|
+
oneofKind: "partyPreferences",
|
|
730
|
+
partyPreferences: proto_PartyPreferencesPayload.create({
|
|
731
|
+
...preferences,
|
|
732
|
+
shoppingList: pushedShoppingList(shoppingList, false),
|
|
733
|
+
}),
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// One order as a message to push. Its shopping list goes out final whatever was passed in, which is
|
|
738
|
+
// what Java SalesOrderViewModel.saveChanges does before creating one: an order is what a customer
|
|
739
|
+
// has finished choosing, and only a final list tracks a later change as a change rather than
|
|
740
|
+
// silently altering what was ordered.
|
|
741
|
+
// The two totals are filled in here from the list, and are what a fulfilled order is left with once
|
|
742
|
+
// the business has cleared the list itself - so they are written off the items actually being sent
|
|
743
|
+
// rather than off the counts the list carries, which a caller could have set to anything.
|
|
744
|
+
export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, creationDate) {
|
|
745
|
+
return proto_Message.create({
|
|
746
|
+
entity: pushedEntity(orderId, creationDate, creationDate),
|
|
747
|
+
senderId: senderId,
|
|
748
|
+
messageType: proto_MessageType.SALES_ORDER,
|
|
749
|
+
status: proto_TaskStatus.NOT_YET_PROCESSED,
|
|
750
|
+
payload: {
|
|
751
|
+
oneofKind: "salesOrder",
|
|
752
|
+
salesOrder: proto_SalesOrderPayload.create({
|
|
753
|
+
status: proto_OrderStatus.NOT_STARTED,
|
|
754
|
+
fulfillmentMode: fulfillmentMode,
|
|
755
|
+
shoppingList: pushedShoppingList(shoppingList, true),
|
|
756
|
+
shoppingListTotal: amountToScaledValue(shoppingList.totalAmount),
|
|
757
|
+
shoppingListCount: shoppingList.existingItems.length,
|
|
758
|
+
deliveryAddressId: deliveryAddressId,
|
|
759
|
+
deliveryAddress: deliveryAddress === null ? undefined : addressToProto(deliveryAddress),
|
|
760
|
+
creationDate: creationDate,
|
|
761
|
+
note: stringToProto(note),
|
|
762
|
+
}),
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// A sync in response's messages as the cache write they make, tombstones included - the same as any
|
|
767
|
+
// other entity array this library reads off the wire. This is how a push's own messages end up
|
|
768
|
+
// stored under the update numbers the server just assigned them, with no separate stamping step:
|
|
769
|
+
// the response already carries them as full entities, not as a bare number to reapply by hand.
|
|
770
|
+
export function messagesToCacheWrite(messages) {
|
|
771
|
+
return toCacheWrite(messages.map((message) => toWireEntity(message.entity, proto_Message.toBinary(message))));
|
|
772
|
+
}
|
|
773
|
+
// The highest update number among this customer's messages already cached, cart and orders alike -
|
|
774
|
+
// what a push sends as its cursor. Computed fresh off the store each time rather than kept anywhere:
|
|
775
|
+
// no cursor is persisted in this library, and this is no exception - see THE SYNC STATES in
|
|
776
|
+
// ../Prompt.txt. Zero when none are cached, which is how a first push asks for everything back.
|
|
777
|
+
export function highestCachedMessageUpdateNumber(blobs) {
|
|
778
|
+
let highest = 0n;
|
|
779
|
+
for (const message of messagesFromBlobs(blobs)) {
|
|
780
|
+
const updateNumber = getEntity(message.entity).updateNumber;
|
|
781
|
+
if (updateNumber > highest)
|
|
782
|
+
highest = updateNumber;
|
|
783
|
+
}
|
|
784
|
+
return highest;
|
|
785
|
+
}
|
|
786
|
+
// The Entity of something this client is pushing. The update number goes out as zero: the protocol
|
|
787
|
+
// replaces a caller's own with the server's and answers what it assigned, so there is nothing to
|
|
788
|
+
// send and nothing to guess. updateInfo goes out as zero too, which is not the same kind of zero -
|
|
789
|
+
// it should be the id of this store, and this store has no id yet, so a read of this business will
|
|
790
|
+
// send these messages back rather than recognising them as its own. That costs bandwidth and
|
|
791
|
+
// nothing else, since the eTag of the copy stored here is the one the server kept.
|
|
792
|
+
function pushedEntity(id, createdAt, updatedAt) {
|
|
793
|
+
return proto_Entity.create({ id: id, createdAt: createdAt, updatedAt: updatedAt });
|
|
794
|
+
}
|
|
795
|
+
// Whether a cart is worth the request, answered off what the store already holds - so a caller may
|
|
796
|
+
// call updateCart as often as it likes and only a cart the server has not got costs anything.
|
|
797
|
+
// Two ways it is worth sending. The list differs from the stored one, which is the ordinary case.
|
|
798
|
+
// Or the stored copy has never been accepted: a pushed entity goes out with updateNumber zero and
|
|
799
|
+
// comes back carrying the server's, so zero here means this cart was written by
|
|
800
|
+
// Model.updateCartLocalOnly and has not been sent since.
|
|
801
|
+
// The lists are compared rather than the messages, because a message always differs - updatedAt is
|
|
802
|
+
// the clock's reading, and buildCartMessage moves it on every call. Comparing them is sound only
|
|
803
|
+
// because the server keeps a pushed list exactly as it arrived, so what comes back is what went
|
|
804
|
+
// out; a server that normalised the list would make every call differ and push every time, which
|
|
805
|
+
// is what this library did before and so is no worse.
|
|
806
|
+
export function isCartWorthPushing(stored, cartMessage) {
|
|
807
|
+
// Nothing stored and nothing to send: an emptied cart and one never created are one state
|
|
808
|
+
if (stored === null)
|
|
809
|
+
return getCartShoppingList(cartMessage) !== undefined;
|
|
810
|
+
if (getEntity(stored.entity).updateNumber === 0n)
|
|
811
|
+
return true;
|
|
812
|
+
return !proto_ShoppingList.equals(getCartShoppingList(stored), getCartShoppingList(cartMessage));
|
|
813
|
+
}
|
|
814
|
+
// The list inside a cart message, or absent for one carrying no list - which is an emptied cart
|
|
815
|
+
function getCartShoppingList(message) {
|
|
816
|
+
return message.payload.oneofKind === "partyPreferences"
|
|
817
|
+
? message.payload.partyPreferences.shoppingList
|
|
818
|
+
: undefined;
|
|
819
|
+
}
|
|
820
|
+
// A shopping list on its way out, or absent when it holds nothing - see buildCartMessage. Emptiness
|
|
821
|
+
// is judged by the items being sent and not by the counts the list carries, for the reason the
|
|
822
|
+
// counts below are.
|
|
823
|
+
function pushedShoppingList(shoppingList, isFinal) {
|
|
824
|
+
if (shoppingList === null)
|
|
825
|
+
return undefined;
|
|
826
|
+
if (shoppingList.existingItems.length === 0 && shoppingList.deletedItems.length === 0)
|
|
827
|
+
return undefined;
|
|
828
|
+
return shoppingListToProto(shoppingList, isFinal);
|
|
829
|
+
}
|
|
830
|
+
// The counts are written off the arrays rather than copied from the list. A read takes them from the
|
|
831
|
+
// wire because the protocol sends a count even where it does not send the items, but a push always
|
|
832
|
+
// sends the items - so the arrays are what is true here, and taking the counts from them is what
|
|
833
|
+
// makes it impossible to push a list whose count disagrees with its contents.
|
|
834
|
+
function shoppingListToProto(shoppingList, isFinal) {
|
|
835
|
+
return proto_ShoppingList.create({
|
|
836
|
+
isFinal: isFinal,
|
|
837
|
+
autoAddDeliveryCharge: shoppingList.autoAddDeliveryCharge,
|
|
838
|
+
autoAddServiceCharge: shoppingList.autoAddServiceCharge,
|
|
839
|
+
inventoryLocationId: shoppingList.inventoryLocationId,
|
|
840
|
+
priceListId: shoppingList.priceListId,
|
|
841
|
+
totalAmountBeforeDiscount: amountToScaledValue(shoppingList.totalAmountBeforeDiscount),
|
|
842
|
+
discount: amountToScaledValue(shoppingList.discount),
|
|
843
|
+
existingItemCount: shoppingList.existingItems.length,
|
|
844
|
+
deletedItemCount: shoppingList.deletedItems.length,
|
|
845
|
+
existingItems: shoppingList.existingItems.map(shoppingListItemToProto),
|
|
846
|
+
deletedItems: shoppingList.deletedItems.map(shoppingListItemToProto),
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
// The mirror of shoppingListItemFromProto: everything the model holds unscaled goes back scaled, and
|
|
850
|
+
// nothing derived is sent - amount, totalTax and finalPrice are computed from these fields, here and
|
|
851
|
+
// by the server both.
|
|
852
|
+
function shoppingListItemToProto(item) {
|
|
853
|
+
return proto_ShoppingListItem.create({
|
|
854
|
+
packageId: item.packageId,
|
|
855
|
+
quantity: quantityToScaledValue(item.quantity),
|
|
856
|
+
unitPrice: amountToScaledValue(item.unitPrice),
|
|
857
|
+
shoppingListOrder: item.shoppingListOrder,
|
|
858
|
+
isOriginal: item.isOriginal,
|
|
859
|
+
isModified: item.isModified,
|
|
860
|
+
isStockKeepingRequired: item.isStockKeepingRequired,
|
|
861
|
+
priceDiscount: amountToScaledValue(item.priceDiscount),
|
|
862
|
+
discount: amountToScaledValue(item.discount),
|
|
863
|
+
taxes: item.taxes.map(taxToProto),
|
|
864
|
+
expiry: item.expiryMinutes,
|
|
865
|
+
description: stringToProto(item.description),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function taxToProto(tax) {
|
|
869
|
+
return proto_Tax.create({ type: tax.type, rate: taxRateToScaledValue(tax.rate) });
|
|
870
|
+
}
|
|
871
|
+
// #endregion
|
|
872
|
+
// #region Entity and item bookkeeping
|
|
873
|
+
// One pass over the entities of a response, splitting them into what is stored and what is removed:
|
|
874
|
+
// a live entity becomes a blob, and a tombstone becomes an id to delete. The blob id is the entity
|
|
875
|
+
// id, which is what makes one read of a container enough to sort it by entity type, and the eTag is
|
|
876
|
+
// Entity.updatedAt.
|
|
877
|
+
// Mutable arrays, so that businessResponseToCacheWrite can add the attachment base url to the blobs.
|
|
878
|
+
function toCacheWrite(entities) {
|
|
879
|
+
const blobs = [];
|
|
880
|
+
const deletedIds = [];
|
|
881
|
+
for (const entity of entities) {
|
|
882
|
+
if (entity.isDeleted)
|
|
883
|
+
deletedIds.push(entity.id);
|
|
884
|
+
else
|
|
885
|
+
blobs.push({ id: entity.id, eTag: entity.updatedAt, bytes: entity.bytes });
|
|
886
|
+
}
|
|
887
|
+
return { blobs: blobs, deletedIds: deletedIds };
|
|
888
|
+
}
|
|
889
|
+
function messageToBlob(message) {
|
|
890
|
+
const entity = getEntity(message.entity);
|
|
891
|
+
return {
|
|
892
|
+
id: entity.id,
|
|
893
|
+
eTag: entity.updatedAt,
|
|
894
|
+
bytes: proto_Message.toBinary(message),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function messagesFromBlobs(blobs) {
|
|
898
|
+
const result = [];
|
|
899
|
+
for (const blob of blobs) {
|
|
900
|
+
if (blob.id < 0n)
|
|
901
|
+
continue; // A fixed blob, not an entity
|
|
902
|
+
if (Ids.getEntityTypeId(blob.id) !== proto_Constants.MESSAGE_ENTITY_TYPE_ID)
|
|
903
|
+
continue;
|
|
904
|
+
result.push(proto_Message.fromBinary(blob.bytes));
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
function toWireEntity(entity, bytes) {
|
|
909
|
+
if (entity === undefined)
|
|
910
|
+
throw new InvalidArgumentException("An entity of the response carries no Entity");
|
|
911
|
+
const isDeleted = isDeletedEntity(entity);
|
|
912
|
+
return {
|
|
913
|
+
id: entity.id,
|
|
914
|
+
entityTypeId: Ids.getEntityTypeId(entity.id),
|
|
915
|
+
updateNumber: entity.updateNumber,
|
|
916
|
+
updatedAt: entity.updatedAt,
|
|
917
|
+
isDeleted: isDeleted,
|
|
918
|
+
bytes: isDeleted ? EMPTY_BYTES : bytes,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
// A tombstone: the update type is held in the last two bits of the update number
|
|
922
|
+
function isDeletedEntity(entity) {
|
|
923
|
+
return ((entity.updateNumber & BigInt(proto_Constants.UPDATE_TYPE_MASK)) ===
|
|
924
|
+
BigInt(proto_Constants.UPDATE_TYPE_DELETED));
|
|
925
|
+
}
|
|
926
|
+
function isDeletedItem(item) {
|
|
927
|
+
return isDeletedEntity(getEntity(getItem(item).entity));
|
|
928
|
+
}
|
|
929
|
+
function toItemWireEntity(item, bytes) {
|
|
930
|
+
if (item === undefined)
|
|
931
|
+
throw new InvalidArgumentException("An item of the response carries no Item");
|
|
932
|
+
return toWireEntity(item.entity, bytes);
|
|
933
|
+
}
|
|
934
|
+
// The five fields every entity of the protocol carries, as one value - see EntityHeader
|
|
935
|
+
function toEntityHeader(entity) {
|
|
936
|
+
return new EntityHeader(entity.id, entity.createdAt, entity.updatedAt, entity.updateNumber, entity.updateInfo);
|
|
937
|
+
}
|
|
938
|
+
function getEntity(entity) {
|
|
939
|
+
if (entity === undefined)
|
|
940
|
+
throw new InvalidArgumentException("A stored entity carries no Entity");
|
|
941
|
+
return entity;
|
|
942
|
+
}
|
|
943
|
+
function getItem(item) {
|
|
944
|
+
if (item === undefined)
|
|
945
|
+
throw new InvalidArgumentException("A stored item carries no Item");
|
|
946
|
+
return item;
|
|
947
|
+
}
|
|
948
|
+
function getItemId(item) {
|
|
949
|
+
return getEntity(getItem(item).entity).id;
|
|
950
|
+
}
|
|
951
|
+
// #endregion
|
|
952
|
+
// #region Values
|
|
953
|
+
// An absent optional string is null and not empty, because Java tells the two apart
|
|
954
|
+
function stringFromProto(value) {
|
|
955
|
+
return value === undefined ? null : value;
|
|
956
|
+
}
|
|
957
|
+
// The way back. An empty string goes out absent as well: the two mean the same thing to a caller
|
|
958
|
+
// that had nothing to say, and sending one of them would have the field come back as the other.
|
|
959
|
+
function stringToProto(value) {
|
|
960
|
+
return value === null || value.length === 0 ? undefined : value;
|
|
961
|
+
}
|
|
962
|
+
// An absent optional bool is null - for Product.isManufacturedByBusiness that means "not said either way"
|
|
963
|
+
function booleanFromProto(value) {
|
|
964
|
+
return value === undefined ? null : value;
|
|
965
|
+
}
|
|
966
|
+
function attachmentBaseUrlFromProto(urlAndToken) {
|
|
967
|
+
return new AttachmentBaseUrl(urlAndToken.resourceUrl, urlAndToken.sasToken, urlAndToken.expiry);
|
|
968
|
+
}
|
|
969
|
+
function addressFromProto(address) {
|
|
970
|
+
return new Address(address.id, address.addressType, stringFromProto(address.unitNumber), stringFromProto(address.community), stringFromProto(address.line2), stringFromProto(address.city), stringFromProto(address.state), stringFromProto(address.pinCode), address.countryCode);
|
|
971
|
+
}
|
|
972
|
+
// The way back, for a delivery address a customer sends out directly rather than by id - see
|
|
973
|
+
// buildOrderMessage. id is sent as whatever Address carries, though it means nothing outside the
|
|
974
|
+
// list it was read from and this address belongs to none.
|
|
975
|
+
function addressToProto(address) {
|
|
976
|
+
return proto_Address.create({
|
|
977
|
+
id: address.id,
|
|
978
|
+
addressType: address.addressType,
|
|
979
|
+
unitNumber: stringToProto(address.unitNumber),
|
|
980
|
+
community: stringToProto(address.community),
|
|
981
|
+
line2: stringToProto(address.line2),
|
|
982
|
+
city: stringToProto(address.city),
|
|
983
|
+
state: stringToProto(address.state),
|
|
984
|
+
pinCode: stringToProto(address.pinCode),
|
|
985
|
+
countryCode: address.countryCode,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
// The way back, for the addresses of a user being sent to the server rather than read from it.
|
|
989
|
+
function addressListToProto(addressList) {
|
|
990
|
+
return proto_AddressList.create({
|
|
991
|
+
defaultAddressId: addressList.defaultAddressId,
|
|
992
|
+
addresses: addressList.addresses.map(addressToProto),
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
function addressListFromProto(addressList) {
|
|
996
|
+
if (addressList === undefined)
|
|
997
|
+
return AddressList.EMPTY;
|
|
998
|
+
return new AddressList(addressList.defaultAddressId, Object.freeze(addressList.addresses.map(addressFromProto)));
|
|
999
|
+
}
|
|
1000
|
+
function taxFromProto(tax) {
|
|
1001
|
+
return Tax.fromScaledValues(tax.type, tax.rate);
|
|
1002
|
+
}
|
|
1003
|
+
function latLongFromProto(latLong) {
|
|
1004
|
+
if (latLong === undefined)
|
|
1005
|
+
return null;
|
|
1006
|
+
return new LatLong(latLong.latitude, latLong.longitude);
|
|
1007
|
+
}
|
|
1008
|
+
function dimensionsFromProto(dimensions) {
|
|
1009
|
+
if (dimensions === undefined)
|
|
1010
|
+
return null;
|
|
1011
|
+
return new Dimensions(dimensions.unit, dimensions.length, dimensions.width, dimensions.height);
|
|
1012
|
+
}
|
|
1013
|
+
function weightFromProto(weight) {
|
|
1014
|
+
if (weight === undefined)
|
|
1015
|
+
return null;
|
|
1016
|
+
return new Weight(weight.unit, weight.value);
|
|
1017
|
+
}
|
|
1018
|
+
function packageIdentifierFromProto(identifier) {
|
|
1019
|
+
return new PackageIdentifier(identifier.type, identifier.value);
|
|
1020
|
+
}
|
|
1021
|
+
function businessIdentifierFromProto(identifier) {
|
|
1022
|
+
return new BusinessIdentifier(identifier.type, identifier.value);
|
|
1023
|
+
}
|
|
1024
|
+
function paymentMethodDetailFromProto(detail) {
|
|
1025
|
+
return new PaymentMethodDetail(detail.method, stringFromProto(detail.data));
|
|
1026
|
+
}
|
|
1027
|
+
function workingHoursRangeFromProto(range) {
|
|
1028
|
+
return new WorkingHoursRange(range.openMinute, range.closeMinute);
|
|
1029
|
+
}
|
|
1030
|
+
function workingHoursFromProto(workingHours) {
|
|
1031
|
+
return new WorkingHours(Object.freeze(workingHours.ranges.map(workingHoursRangeFromProto)));
|
|
1032
|
+
}
|
|
1033
|
+
// An absent WorkingDays means the business works every day, all day - the Java default, not "no day at all"
|
|
1034
|
+
function workingDaysFromProto(workingDays) {
|
|
1035
|
+
if (workingDays === undefined)
|
|
1036
|
+
return WorkingDays.ALL_DAYS;
|
|
1037
|
+
return new WorkingDays(Object.freeze([...workingDays.days]), Object.freeze(workingDays.workingHours.map(workingHoursFromProto)));
|
|
1038
|
+
}
|
|
1039
|
+
function shoppingListItemFromProto(item) {
|
|
1040
|
+
return ShoppingListItem.fromScaledValues(item.packageId, item.quantity, item.unitPrice, item.shoppingListOrder, item.isOriginal, item.isModified, item.isStockKeepingRequired, item.priceDiscount, item.discount, Object.freeze(item.taxes.map(taxFromProto)), item.expiry, stringFromProto(item.description));
|
|
1041
|
+
}
|
|
1042
|
+
function shoppingListFromProto(shoppingList) {
|
|
1043
|
+
if (shoppingList === undefined)
|
|
1044
|
+
return ShoppingList.EMPTY;
|
|
1045
|
+
return ShoppingList.fromScaledValues(shoppingList.isFinal, shoppingList.autoAddDeliveryCharge, shoppingList.autoAddServiceCharge, shoppingList.inventoryLocationId, shoppingList.priceListId, shoppingList.totalAmountBeforeDiscount, shoppingList.discount, shoppingList.existingItemCount, shoppingList.deletedItemCount, Object.freeze(shoppingList.existingItems.map(shoppingListItemFromProto)), Object.freeze(shoppingList.deletedItems.map(shoppingListItemFromProto)));
|
|
1046
|
+
}
|
|
1047
|
+
function generalSettingsFromProto(settings) {
|
|
1048
|
+
return GeneralSettings.fromScaledValues(Object.freeze(settings.paymentMethodDetails.map(paymentMethodDetailFromProto)), stringFromProto(settings.customerServicePhone), settings.isWhatsAppSupported, settings.isSmsSupported, addressListFromProto(settings.businessAddresses), Object.freeze(settings.businessIdentifiers.map(businessIdentifierFromProto)), Object.freeze([...settings.fulfillmentModes]), settings.deliveryCharge, Object.freeze(settings.deliveryChargeTaxes.map(taxFromProto)), settings.freeDeliveryTotal, settings.deliveryRadiusKms, stringFromProto(settings.storeTagline), workingDaysFromProto(settings.workingDays), latLongFromProto(settings.primaryStoreLocation), stringFromProto(settings.googleBusinessCategoryId), stringFromProto(settings.googleBusinessCategoryName), stringFromProto(settings.onlineStoreThemeId));
|
|
1049
|
+
}
|
|
1050
|
+
// #endregion
|
|
1051
|
+
// #region Entities
|
|
1052
|
+
function businessFromProto(businessProto) {
|
|
1053
|
+
const entity = getEntity(businessProto.entity);
|
|
1054
|
+
return new Business(toEntityHeader(entity), businessProto.businessId, businessProto.displayName, businessProto.countryCode);
|
|
1055
|
+
}
|
|
1056
|
+
function userProfileFromProto(userProto) {
|
|
1057
|
+
const entity = getEntity(userProto.entity);
|
|
1058
|
+
return new UserProfile(toEntityHeader(entity), userProto.firstName, stringFromProto(userProto.lastName), userProto.mobilePhoneNumber, addressListFromProto(userProto.addresses), stringFromProto(userProto.preferredLanguageCode));
|
|
1059
|
+
}
|
|
1060
|
+
// The profile database copy of a business - see BusinessProfile for why this is not a Business
|
|
1061
|
+
function businessProfileFromProto(businessProto) {
|
|
1062
|
+
const entity = getEntity(businessProto.entity);
|
|
1063
|
+
return new BusinessProfile(toEntityHeader(entity), businessProto.businessId, businessProto.displayName, businessProto.countryCode);
|
|
1064
|
+
}
|
|
1065
|
+
function businessLinkFromProto(businessLinkProto) {
|
|
1066
|
+
const entity = getEntity(businessLinkProto.entity);
|
|
1067
|
+
return new BusinessLink(toEntityHeader(entity), businessLinkProto.linkedEntityId, businessLinkProto.businessId, businessLinkProto.role, businessLinkProto.partyId, stringFromProto(businessLinkProto.linkToken), businessLinkProto.expiry);
|
|
1068
|
+
}
|
|
1069
|
+
function attachmentFromProto(attachmentProto) {
|
|
1070
|
+
const entity = getEntity(attachmentProto.entity);
|
|
1071
|
+
return new Attachment(toEntityHeader(entity), attachmentProto.attachmentType, attachmentProto.owningEntityId, attachmentProto.fileHash ?? null, stringFromProto(attachmentProto.fileExtension), stringFromProto(attachmentProto.fileName));
|
|
1072
|
+
}
|
|
1073
|
+
function productCategoryFromProto(productCategoryProto) {
|
|
1074
|
+
const item = getItem(productCategoryProto.item);
|
|
1075
|
+
const entity = getEntity(item.entity);
|
|
1076
|
+
return new ProductCategory(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled);
|
|
1077
|
+
}
|
|
1078
|
+
function priceListFromProto(priceListProto) {
|
|
1079
|
+
const item = getItem(priceListProto.item);
|
|
1080
|
+
const entity = getEntity(item.entity);
|
|
1081
|
+
return new PriceList(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled);
|
|
1082
|
+
}
|
|
1083
|
+
function priceFromProto(priceProto) {
|
|
1084
|
+
const item = getItem(priceProto.item);
|
|
1085
|
+
const entity = getEntity(item.entity);
|
|
1086
|
+
return Price.fromScaledValues(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, priceProto.packageId, priceProto.regularPrice, priceProto.offerPrice);
|
|
1087
|
+
}
|
|
1088
|
+
function packageFromProto(packageProto, photos, price) {
|
|
1089
|
+
const item = getItem(packageProto.item);
|
|
1090
|
+
const entity = getEntity(item.entity);
|
|
1091
|
+
return new Package(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, packageProto.isSoldLoose, packageProto.itemCode, Object.freeze(packageProto.identifiers.map(packageIdentifierFromProto)), dimensionsFromProto(packageProto.dimensions), weightFromProto(packageProto.weight), photos, price);
|
|
1092
|
+
}
|
|
1093
|
+
function productFromProto(productProto, photos, packages) {
|
|
1094
|
+
const item = getItem(productProto.item);
|
|
1095
|
+
const entity = getEntity(item.entity);
|
|
1096
|
+
return new Product(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, productProto.productType, stringFromProto(productProto.brand), booleanFromProto(productProto.isManufacturedByBusiness), stringFromProto(productProto.manufacturer), productProto.manufacturerAddress === undefined
|
|
1097
|
+
? null
|
|
1098
|
+
: addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map(taxFromProto)), Object.freeze([...productProto.features]), photos, packages);
|
|
1099
|
+
}
|
|
1100
|
+
function orderFromProto(message, salesOrder) {
|
|
1101
|
+
const entity = getEntity(message.entity);
|
|
1102
|
+
return Order.fromScaledValues(toEntityHeader(entity), message.senderId, message.status, stringFromProto(message.senderName), stringFromProto(message.senderPhone), salesOrder.status, salesOrder.fulfillmentMode, shoppingListFromProto(salesOrder.shoppingList), salesOrder.shoppingListTotal, salesOrder.shoppingListCount, salesOrder.deliveryAddressId, salesOrder.deliveryAddress === undefined ? null : addressFromProto(salesOrder.deliveryAddress), salesOrder.creationDate, stringFromProto(salesOrder.note), salesOrder.paymentReceived);
|
|
1103
|
+
}
|
|
1104
|
+
// The one part of a cart message a customer is shown. Absent rather than empty when the cart holds
|
|
1105
|
+
// nothing, so that "emptied by placing an order" and "never had one" are the same answer - which is
|
|
1106
|
+
// also what makes createOrder's emptied cart indistinguishable from a cart that was never created.
|
|
1107
|
+
// Everything else the message carries is the server's: who sent it, how far the business has got
|
|
1108
|
+
// with reading it, and the fulfillment mode Java keeps there, which belongs to an order here.
|
|
1109
|
+
function cartShoppingListFromProto(partyPreferences) {
|
|
1110
|
+
const shoppingList = partyPreferences.shoppingList;
|
|
1111
|
+
return shoppingList === undefined ? null : shoppingListFromProto(shoppingList);
|
|
1112
|
+
}
|
|
1113
|
+
// #endregion
|
|
1114
|
+
// #region Photos
|
|
1115
|
+
// The photos of one entity, in no particular order - a UI orders these for display. Each is built
|
|
1116
|
+
// with the base url resolved for this response, since a Photo takes it at construction rather than
|
|
1117
|
+
// looking it up later.
|
|
1118
|
+
function takePhotos(attachmentsByOwner, owningEntityId, attachmentBaseUrl) {
|
|
1119
|
+
const attachments = attachmentsByOwner.get(owningEntityId);
|
|
1120
|
+
if (attachments === undefined)
|
|
1121
|
+
return NO_PHOTOS;
|
|
1122
|
+
return Object.freeze(attachments.map((attachment) => new Photo(attachment, attachmentBaseUrl)));
|
|
1123
|
+
}
|
|
1124
|
+
// #endregion
|
|
1125
|
+
const EMPTY_BYTES = new Uint8Array(0);
|
|
1126
|
+
const NO_PHOTOS = Object.freeze([]);
|