@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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
3
|
+
import { ReservedPackage as proto_ReservedPackage } from "./generated/ReservedPackage";
|
|
4
|
+
import { MERABILLS_ONLINE_PRICE_LIST_ID } from "./Constants";
|
|
5
|
+
import { InvalidArgumentException } from "./Exceptions";
|
|
6
|
+
import Ids from "./Ids";
|
|
7
|
+
import ShoppingList, { ShoppingListItem } from "./ShoppingList";
|
|
8
|
+
// Builds the cart a customer is filling, in the terms a UI has to hand: packages rather than
|
|
9
|
+
// package ids, quantities rather than quantities multiplied by Constants.QUANTITY_FACTOR, and
|
|
10
|
+
// prices taken off the catalog rather than worked out and passed in. Get one from
|
|
11
|
+
// Business.createCartEditor, edit it, then hand build() to Model.updateCartLocalOnly.
|
|
12
|
+
//
|
|
13
|
+
// Mutable, and deliberately the only mutable thing in this object model: it is a scratch copy of
|
|
14
|
+
// the cart, not the cart. Nothing here writes anything - editing an editor changes no Business, no
|
|
15
|
+
// store and nothing on the server until a built list is pushed. It is also a snapshot: it reads the
|
|
16
|
+
// catalog, settings and cart once, at construction, so a sync arriving mid-edit does not move
|
|
17
|
+
// prices under the customer. Make a fresh one when the Business republishes.
|
|
18
|
+
class CartEditor {
|
|
19
|
+
// Business.createCartEditor is how a UI gets one of these; this takes the three things it needs
|
|
20
|
+
// rather than the Business itself, so a caller can build a cart against a catalog and settings
|
|
21
|
+
// that are not the ones currently published.
|
|
22
|
+
//
|
|
23
|
+
// Every line is priced off the catalog now, not off what the cart was holding: a cart lives
|
|
24
|
+
// until it is ordered, the merchant may have repriced in between, and what an order freezes is
|
|
25
|
+
// the price at the moment it is placed. Lines the catalog can no longer price - a package
|
|
26
|
+
// withdrawn, disabled, or left without a price - are dropped, and left in unavailableItems for a
|
|
27
|
+
// UI to tell the customer about. The charge lines of the list it started from (see
|
|
28
|
+
// ShoppingListItem.isReservedPackage) are dropped too, being this class's to work out rather
|
|
29
|
+
// than a customer's to carry: build() puts back whatever the settings now say.
|
|
30
|
+
constructor(_catalog, _settings, cart) {
|
|
31
|
+
this._catalog = _catalog;
|
|
32
|
+
this._settings = _settings;
|
|
33
|
+
// Insertion ordered, which is what keeps a cart in the order the customer filled it and what
|
|
34
|
+
// gives each line its shoppingListOrder
|
|
35
|
+
this._quantities = new Map();
|
|
36
|
+
this._inventoryLocationId = cart?.inventoryLocationId ?? 0;
|
|
37
|
+
const unavailableItems = [];
|
|
38
|
+
for (const item of cart?.existingItems ?? []) {
|
|
39
|
+
if (item.isReservedPackage)
|
|
40
|
+
continue;
|
|
41
|
+
const buyable = this.getBuyablePackage(item.packageId);
|
|
42
|
+
if (buyable === null)
|
|
43
|
+
unavailableItems.push(item);
|
|
44
|
+
else
|
|
45
|
+
this._quantities.set(buyable.id, item.quantity);
|
|
46
|
+
}
|
|
47
|
+
this._unavailableItems = Object.freeze(unavailableItems);
|
|
48
|
+
}
|
|
49
|
+
// What the customer has picked, in the order it was picked in, each line priced as it would be
|
|
50
|
+
// bought right now. Rebuilt on every read, so a line held across an edit is stale.
|
|
51
|
+
get lines() {
|
|
52
|
+
const lines = [];
|
|
53
|
+
for (const [packageId, quantity] of this._quantities) {
|
|
54
|
+
const buyable = this.getBuyablePackage(packageId);
|
|
55
|
+
if (buyable === null)
|
|
56
|
+
continue; // Cannot happen: only a buyable package is ever put in
|
|
57
|
+
lines.push(new CartLine(buyable, this._catalog.getProduct(buyable.productId), quantity));
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze(lines);
|
|
60
|
+
}
|
|
61
|
+
// The lines of the cart this editor started from that its catalog can no longer price - see the
|
|
62
|
+
// note on the constructor. Empty for a cart built from nothing, and never added to by an edit.
|
|
63
|
+
get unavailableItems() {
|
|
64
|
+
return this._unavailableItems;
|
|
65
|
+
}
|
|
66
|
+
get isEmpty() {
|
|
67
|
+
return this._quantities.size === 0;
|
|
68
|
+
}
|
|
69
|
+
// How many lines the cart has, which is what a cart badge counts - not how many units are in it
|
|
70
|
+
get lineCount() {
|
|
71
|
+
return this._quantities.size;
|
|
72
|
+
}
|
|
73
|
+
// Every unit of every line added up. Fractional where a package is sold loose.
|
|
74
|
+
get totalQuantity() {
|
|
75
|
+
let result = 0;
|
|
76
|
+
for (const quantity of this._quantities.values())
|
|
77
|
+
result += quantity;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Zero for a package that is not in the cart
|
|
81
|
+
getQuantity(item) {
|
|
82
|
+
return this._quantities.get(item.id) ?? 0;
|
|
83
|
+
}
|
|
84
|
+
// Sets how much of one package the cart holds; zero takes it out. A package a customer cannot
|
|
85
|
+
// buy - one disabled, or priced in no list this storefront sells from - is refused, as is a
|
|
86
|
+
// fractional quantity of a package that is not sold loose, and a quantity past what the wire can
|
|
87
|
+
// carry. Returns this, so edits chain.
|
|
88
|
+
setQuantity(item, quantity) {
|
|
89
|
+
if (!Number.isFinite(quantity) || quantity < 0)
|
|
90
|
+
throw new InvalidArgumentException(`${quantity} is not a quantity`);
|
|
91
|
+
if (quantity === 0) {
|
|
92
|
+
this._quantities.delete(item.id);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
if (this.getBuyablePackage(item.id) === null)
|
|
96
|
+
throw new InvalidArgumentException(`Package ${Ids.toStorageHexString(item.id)} cannot be bought from this catalog`);
|
|
97
|
+
if (!item.isSoldLoose && !Number.isInteger(quantity))
|
|
98
|
+
throw new InvalidArgumentException(`${item.name} is not sold loose, so it cannot be bought ${quantity} at a time`);
|
|
99
|
+
// A quantity travels as an int32 multiplied by Constants.QUANTITY_FACTOR, so this is the most
|
|
100
|
+
// of one package a cart can hold - and it is refused here rather than silently wrapping there.
|
|
101
|
+
if (quantity > CartEditor.MAX_QUANTITY)
|
|
102
|
+
throw new InvalidArgumentException(`${quantity} is more than the ${CartEditor.MAX_QUANTITY} the protocol can carry`);
|
|
103
|
+
if (!this._quantities.has(item.id) &&
|
|
104
|
+
this._quantities.size >= proto_Constants.SHOPPING_LIST_MAX_ITEMS)
|
|
105
|
+
throw new InvalidArgumentException(`A cart holds at most ${proto_Constants.SHOPPING_LIST_MAX_ITEMS} lines`);
|
|
106
|
+
this._quantities.set(item.id, quantity);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
// Adds to what the cart already holds of this package, which is what a quantity stepper does.
|
|
110
|
+
// A negative amount takes away, and never below nothing.
|
|
111
|
+
add(item, quantity = 1) {
|
|
112
|
+
return this.setQuantity(item, Math.max(0, this.getQuantity(item) + quantity));
|
|
113
|
+
}
|
|
114
|
+
remove(item) {
|
|
115
|
+
return this.setQuantity(item, 0);
|
|
116
|
+
}
|
|
117
|
+
clear() {
|
|
118
|
+
this._quantities.clear();
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
// What the packages cost before tax, offers already taken off
|
|
122
|
+
get itemsTotal() {
|
|
123
|
+
let result = 0;
|
|
124
|
+
for (const line of this.lines)
|
|
125
|
+
result += line.amount;
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
// What the offers took off, which is what a bill shows as savings. Zero when nothing is on offer.
|
|
129
|
+
get savings() {
|
|
130
|
+
let result = 0;
|
|
131
|
+
for (const line of this.lines)
|
|
132
|
+
result += line.savings;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
get itemsTax() {
|
|
136
|
+
let result = 0;
|
|
137
|
+
for (const line of this.lines)
|
|
138
|
+
result += line.totalTax;
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
// The packages with their tax, and what the free delivery threshold is measured against - the
|
|
142
|
+
// same total Java ShoppingCartViewModel.calculateDeliveryCharge compares, less its delivery line
|
|
143
|
+
get itemsFinalTotal() {
|
|
144
|
+
let result = 0;
|
|
145
|
+
for (const line of this.lines)
|
|
146
|
+
result += line.finalPrice;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// True when this business delivers at all. False means no delivery charge exists to add, whatever
|
|
150
|
+
// build() is asked for, and a UI has no fulfillment choice to offer either.
|
|
151
|
+
get isDeliveryOffered() {
|
|
152
|
+
return this._settings.fulfillmentModes.includes(proto_FulfillmentMode.DELIVERY);
|
|
153
|
+
}
|
|
154
|
+
// What delivering this cart would cost before tax: the business's flat charge, or zero where it
|
|
155
|
+
// has been waived by the cart reaching freeDeliveryTotal, or where the business delivers free
|
|
156
|
+
// anyway. Always zero when the business does not deliver.
|
|
157
|
+
get deliveryCharge() {
|
|
158
|
+
return this.getDeliveryCharge(this.itemsFinalTotal);
|
|
159
|
+
}
|
|
160
|
+
// What delivery costs before any waiver - the price a bill strikes through when it reads FREE.
|
|
161
|
+
// Zero where the business delivers free anyway, in which case there is nothing to strike through.
|
|
162
|
+
get regularDeliveryCharge() {
|
|
163
|
+
if (!this.isDeliveryOffered)
|
|
164
|
+
return 0;
|
|
165
|
+
// Java clamps a negative charge to zero rather than treating it as a refund, and so does this
|
|
166
|
+
return Math.max(0, this._settings.deliveryCharge);
|
|
167
|
+
}
|
|
168
|
+
// True when delivering this cart costs nothing, whether because the business never charges or
|
|
169
|
+
// because this cart has reached the threshold
|
|
170
|
+
get isDeliveryFree() {
|
|
171
|
+
return this.isDeliveryOffered && this.deliveryCharge === 0;
|
|
172
|
+
}
|
|
173
|
+
// How much more the customer would have to add for delivery to stop being charged, which is the
|
|
174
|
+
// "add X more for free delivery" note. Zero once it is already free, and null when the business
|
|
175
|
+
// has set no threshold, so there is no amount that would ever make it free.
|
|
176
|
+
get amountToFreeDelivery() {
|
|
177
|
+
if (!this.isDeliveryOffered)
|
|
178
|
+
return null;
|
|
179
|
+
if (this.regularDeliveryCharge === 0)
|
|
180
|
+
return 0; // Already free, and free at any total
|
|
181
|
+
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
182
|
+
if (freeDeliveryTotal <= 0)
|
|
183
|
+
return null; // No threshold: adding more changes nothing
|
|
184
|
+
return Math.max(0, freeDeliveryTotal - this.itemsFinalTotal);
|
|
185
|
+
}
|
|
186
|
+
// Tax on the delivery charge, at the rates GeneralSettings.deliveryChargeTaxes carries - which
|
|
187
|
+
// are the business's own and have nothing to do with a product's. Zero when delivery is free,
|
|
188
|
+
// there being nothing to tax.
|
|
189
|
+
get deliveryTax() {
|
|
190
|
+
return CartEditor.getTax(this.deliveryCharge, this._settings.deliveryChargeTaxes);
|
|
191
|
+
}
|
|
192
|
+
// What the cart comes to. Delivery is left out unless asked for, the way a cart page leaves it
|
|
193
|
+
// out and a checkout page puts it in - the same choice build() takes.
|
|
194
|
+
total(withDeliveryCharge = false) {
|
|
195
|
+
const total = this.itemsFinalTotal;
|
|
196
|
+
if (!withDeliveryCharge || this.isEmpty)
|
|
197
|
+
return total;
|
|
198
|
+
// Worked out from the total already in hand, rather than through the two getters above, each
|
|
199
|
+
// of which would add the lines up again
|
|
200
|
+
const charge = this.getDeliveryCharge(total);
|
|
201
|
+
return total + charge + CartEditor.getTax(charge, this._settings.deliveryChargeTaxes);
|
|
202
|
+
}
|
|
203
|
+
// The cart as a list to write: what Model.updateCartLocalOnly and Model.updateCart take, and
|
|
204
|
+
// what a later Model.createOrder turns into an order.
|
|
205
|
+
//
|
|
206
|
+
// withDeliveryCharge decides whether the delivery charge rides along as a line of its own,
|
|
207
|
+
// carrying ReservedPackage.DELIVERY_CHARGE_PACKAGE and flagged by
|
|
208
|
+
// ShoppingList.autoAddDeliveryCharge. A cart page has no fulfillment chosen yet and so leaves it
|
|
209
|
+
// out, which is the default; a checkout page that has settled on delivery asks for it. The line
|
|
210
|
+
// is added even when the charge has been waived - at zero, with the regular charge as its
|
|
211
|
+
// priceDiscount - so a bill can show FREE with the original struck through, which is what Java
|
|
212
|
+
// ShoppingCartViewModel.setDeliveryCharge writes too.
|
|
213
|
+
//
|
|
214
|
+
// Nothing is added to an empty cart: a list holding a delivery charge and no goods is not an
|
|
215
|
+
// order, and Java removes exactly that line when it is all that is left.
|
|
216
|
+
// The list comes back not final. Making it final is placing an order, which is
|
|
217
|
+
// Model.createOrder's to do.
|
|
218
|
+
build(withDeliveryCharge = false) {
|
|
219
|
+
const items = [];
|
|
220
|
+
for (const line of this.lines)
|
|
221
|
+
items.push(line.toShoppingListItem(items.length));
|
|
222
|
+
const isDeliveryCharged = withDeliveryCharge && items.length > 0 && this.isDeliveryOffered;
|
|
223
|
+
if (isDeliveryCharged)
|
|
224
|
+
items.push(this.buildDeliveryChargeItem(items.length));
|
|
225
|
+
// The total is written off the lines being sent, the way ShoppingList.proto defines it: the
|
|
226
|
+
// sum of their final prices, tax included. There is no list-wide discount - an offer comes off
|
|
227
|
+
// a line, and a storefront gives no other.
|
|
228
|
+
let totalAmountBeforeDiscount = 0;
|
|
229
|
+
for (const item of items)
|
|
230
|
+
totalAmountBeforeDiscount += item.finalPrice;
|
|
231
|
+
return new ShoppingList(false, isDeliveryCharged, false, this._inventoryLocationId, this._catalog.priceList?.id ?? MERABILLS_ONLINE_PRICE_LIST_ID, totalAmountBeforeDiscount, 0, items.length, 0, Object.freeze(items), Object.freeze([]));
|
|
232
|
+
}
|
|
233
|
+
// The delivery charge as a line: one of it, at whatever it costs after any waiver, with the
|
|
234
|
+
// waived amount as its price discount - the same shape as a package bought on offer, which is
|
|
235
|
+
// what lets a bill render it with no special case beyond its label.
|
|
236
|
+
buildDeliveryChargeItem(shoppingListOrder) {
|
|
237
|
+
const charge = this.deliveryCharge;
|
|
238
|
+
return new ShoppingListItem(Ids.buildId(proto_Constants.PACKAGE_ENTITY_TYPE_ID, BigInt(proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE)), 1, charge, shoppingListOrder, false, false, false, this.regularDeliveryCharge - charge, 0, this._settings.deliveryChargeTaxes, 0, null);
|
|
239
|
+
}
|
|
240
|
+
// The charge against a cart that comes to this much, which is the whole of the policy. Takes
|
|
241
|
+
// the total rather than reading it, every caller here having worked it out already.
|
|
242
|
+
getDeliveryCharge(itemsFinalTotal) {
|
|
243
|
+
const charge = this.regularDeliveryCharge;
|
|
244
|
+
if (charge === 0)
|
|
245
|
+
return 0; // Free for everyone, or not delivered at all
|
|
246
|
+
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
247
|
+
if (freeDeliveryTotal <= 0)
|
|
248
|
+
return charge; // Never free, whatever the cart totals
|
|
249
|
+
return itemsFinalTotal >= freeDeliveryTotal ? 0 : charge;
|
|
250
|
+
}
|
|
251
|
+
static getTax(amount, taxes) {
|
|
252
|
+
let result = 0;
|
|
253
|
+
for (const tax of taxes)
|
|
254
|
+
result += (amount * tax.rate) / 100;
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
// The package this id names, and null unless a customer could actually buy it right now
|
|
258
|
+
getBuyablePackage(packageId) {
|
|
259
|
+
const item = this._catalog.getPackage(packageId);
|
|
260
|
+
return item !== null && item.isBuyable ? item : null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// What an int32 quantity multiplied by Constants.QUANTITY_FACTOR reaches
|
|
264
|
+
CartEditor.MAX_QUANTITY = Math.floor(0x7fffffff / proto_Constants.QUANTITY_FACTOR);
|
|
265
|
+
export default CartEditor;
|
|
266
|
+
// One line of a cart being edited: the package, the product it is a variant of, and how much of it
|
|
267
|
+
// - with the prices the catalog gives it now. A read-only view over what CartEditor holds, rebuilt
|
|
268
|
+
// on every read of CartEditor.lines rather than kept.
|
|
269
|
+
export class CartLine {
|
|
270
|
+
constructor(_package, _product, _quantity) {
|
|
271
|
+
this._package = _package;
|
|
272
|
+
this._product = _product;
|
|
273
|
+
this._quantity = _quantity;
|
|
274
|
+
}
|
|
275
|
+
get package() {
|
|
276
|
+
return this._package;
|
|
277
|
+
}
|
|
278
|
+
// Null only for a package whose product is not in the catalog, which a buyable package's never is
|
|
279
|
+
get product() {
|
|
280
|
+
return this._product;
|
|
281
|
+
}
|
|
282
|
+
get quantity() {
|
|
283
|
+
return this._quantity;
|
|
284
|
+
}
|
|
285
|
+
// What one unit costs: the offer price where the package is on offer, else the regular price.
|
|
286
|
+
// A buyable package always has a price, so this is never guessed.
|
|
287
|
+
get unitPrice() {
|
|
288
|
+
return this._package.price?.finalPrice ?? 0;
|
|
289
|
+
}
|
|
290
|
+
get regularUnitPrice() {
|
|
291
|
+
return this._package.price?.regularPrice ?? 0;
|
|
292
|
+
}
|
|
293
|
+
get isOnOffer() {
|
|
294
|
+
return this._package.price?.isOnOffer ?? false;
|
|
295
|
+
}
|
|
296
|
+
// The taxes of the product this package belongs to, which are what the line is taxed at
|
|
297
|
+
get taxes() {
|
|
298
|
+
return this._product?.taxes ?? EMPTY_TAXES;
|
|
299
|
+
}
|
|
300
|
+
// What this line costs before tax
|
|
301
|
+
get amount() {
|
|
302
|
+
return this.unitPrice * this._quantity;
|
|
303
|
+
}
|
|
304
|
+
// What the offer takes off this line
|
|
305
|
+
get savings() {
|
|
306
|
+
return (this.regularUnitPrice - this.unitPrice) * this._quantity;
|
|
307
|
+
}
|
|
308
|
+
get totalTax() {
|
|
309
|
+
let result = 0;
|
|
310
|
+
for (const tax of this.taxes)
|
|
311
|
+
result += (this.amount * tax.rate) / 100;
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
// What this line adds to the cart: what it costs plus the tax on it
|
|
315
|
+
get finalPrice() {
|
|
316
|
+
return this.amount + this.totalTax;
|
|
317
|
+
}
|
|
318
|
+
// This line as the protocol carries it. Only CartEditor.build calls this, which is what assigns
|
|
319
|
+
// the order.
|
|
320
|
+
toShoppingListItem(shoppingListOrder) {
|
|
321
|
+
return new ShoppingListItem(this._package.id, this._quantity, this.unitPrice, shoppingListOrder, false, false, false, this.regularUnitPrice - this.unitPrice, 0, this.taxes, 0, null);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const EMPTY_TAXES = Object.freeze([]);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Item from "./Item";
|
|
2
|
+
import Package from "./Package";
|
|
3
|
+
import PriceList from "./PriceList";
|
|
4
|
+
import Product from "./Product";
|
|
5
|
+
import ProductCategory from "./ProductCategory";
|
|
6
|
+
export default class Catalog {
|
|
7
|
+
private readonly _priceList;
|
|
8
|
+
constructor(categories: ReadonlyArray<ProductCategory>, products: ReadonlyArray<Product>, _priceList: PriceList | null);
|
|
9
|
+
get categories(): ReadonlyArray<ProductCategory>;
|
|
10
|
+
get products(): ReadonlyArray<Product>;
|
|
11
|
+
get priceList(): PriceList | null;
|
|
12
|
+
get productCount(): number;
|
|
13
|
+
get packageCount(): number;
|
|
14
|
+
getCategory(productCategoryId: bigint): ProductCategory | null;
|
|
15
|
+
getProduct(productId: bigint): Product | null;
|
|
16
|
+
getPackage(packageId: bigint): Package | null;
|
|
17
|
+
static comparePackages(left: Package, right: Package): number;
|
|
18
|
+
static compareByName(left: Item, right: Item): number;
|
|
19
|
+
private static compareNames;
|
|
20
|
+
private static compareIds;
|
|
21
|
+
private static comparePrices;
|
|
22
|
+
static readonly EMPTY: Catalog;
|
|
23
|
+
private readonly _categories;
|
|
24
|
+
private readonly _products;
|
|
25
|
+
private readonly _categoryMap;
|
|
26
|
+
private readonly _productMap;
|
|
27
|
+
private readonly _packageMap;
|
|
28
|
+
}
|
package/dist/Catalog.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import { EntityHeader } from "./Entity";
|
|
3
|
+
import ProductCategory from "./ProductCategory";
|
|
4
|
+
// The catalog of a business: its categories, and every product in it. Not an entity and not on
|
|
5
|
+
// the wire - the sync layer rebuilds this hierarchy from the flat item entities of a sync response.
|
|
6
|
+
class Catalog {
|
|
7
|
+
constructor(categories, products, _priceList) {
|
|
8
|
+
this._priceList = _priceList;
|
|
9
|
+
this._categoryMap = new Map();
|
|
10
|
+
this._productMap = new Map();
|
|
11
|
+
this._packageMap = new Map();
|
|
12
|
+
// A dummy category standing in for a product whose parentItemId is Constants.ENTITY_ID_NULL.
|
|
13
|
+
// Its own id is that same ENTITY_ID_NULL, so getCategory(product.productCategoryId) resolves
|
|
14
|
+
// it exactly like a real category - including being left out below, the same as any other
|
|
15
|
+
// category, when nothing buyable ends up in it.
|
|
16
|
+
const uncategorized = new ProductCategory(new EntityHeader(BigInt(proto_Constants.ENTITY_ID_NULL), 0n, 0n, 0n, 0n), "", null, BigInt(proto_Constants.ENTITY_ID_NULL), false);
|
|
17
|
+
this._products = Object.freeze([...products]);
|
|
18
|
+
// Only a buyable product - one a customer can actually order - counts towards a category. A
|
|
19
|
+
// product that cannot be bought right now keeps its place in Catalog.products and stays
|
|
20
|
+
// reachable by getProduct, but does not keep a category alive on its own.
|
|
21
|
+
const productsByCategory = new Map();
|
|
22
|
+
for (const product of this._products) {
|
|
23
|
+
this._productMap.set(product.id, product);
|
|
24
|
+
if (product.isBuyable) {
|
|
25
|
+
const productsOfCategory = productsByCategory.get(product.productCategoryId);
|
|
26
|
+
if (productsOfCategory === undefined)
|
|
27
|
+
productsByCategory.set(product.productCategoryId, [product]);
|
|
28
|
+
else
|
|
29
|
+
productsOfCategory.push(product);
|
|
30
|
+
}
|
|
31
|
+
for (const pkg of product.packages)
|
|
32
|
+
this._packageMap.set(pkg.id, pkg);
|
|
33
|
+
}
|
|
34
|
+
// A category with nothing a customer can order is left out rather than shown empty - there is
|
|
35
|
+
// no point showing it to a customer. Nothing here is special-cased for the dummy: it is left
|
|
36
|
+
// out exactly the same way when nothing buyable is uncategorized.
|
|
37
|
+
const nonEmptyCategories = [];
|
|
38
|
+
for (const category of [...categories, uncategorized]) {
|
|
39
|
+
const categoryProducts = productsByCategory.get(category.id);
|
|
40
|
+
if (categoryProducts === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
category.products = Object.freeze(categoryProducts);
|
|
43
|
+
nonEmptyCategories.push(category);
|
|
44
|
+
}
|
|
45
|
+
this._categories = Object.freeze(nonEmptyCategories);
|
|
46
|
+
for (const category of this._categories)
|
|
47
|
+
this._categoryMap.set(category.id, category);
|
|
48
|
+
}
|
|
49
|
+
// In no particular order - a UI orders these for display
|
|
50
|
+
get categories() {
|
|
51
|
+
return this._categories;
|
|
52
|
+
}
|
|
53
|
+
// Every product of the business, whether or not it is in a category, in no particular order
|
|
54
|
+
get products() {
|
|
55
|
+
return this._products;
|
|
56
|
+
}
|
|
57
|
+
// The one price list a storefront sells from - MeraBills Online - and null until it arrives.
|
|
58
|
+
// Every price in this catalog belongs to it; a price in any other list is not sold here and is
|
|
59
|
+
// left out. A shopping list names this list, or falls back to its id when it has none set.
|
|
60
|
+
get priceList() {
|
|
61
|
+
return this._priceList;
|
|
62
|
+
}
|
|
63
|
+
get productCount() {
|
|
64
|
+
return this._products.length;
|
|
65
|
+
}
|
|
66
|
+
get packageCount() {
|
|
67
|
+
return this._packageMap.size;
|
|
68
|
+
}
|
|
69
|
+
getCategory(productCategoryId) {
|
|
70
|
+
return this._categoryMap.get(productCategoryId) ?? null;
|
|
71
|
+
}
|
|
72
|
+
getProduct(productId) {
|
|
73
|
+
return this._productMap.get(productId) ?? null;
|
|
74
|
+
}
|
|
75
|
+
// Returns null for the four reserved package ids a shopping list uses for its charge and
|
|
76
|
+
// summary lines (see Ids.isReservedPackageId), and for a package whose product was deleted
|
|
77
|
+
getPackage(packageId) {
|
|
78
|
+
return this._packageMap.get(packageId) ?? null;
|
|
79
|
+
}
|
|
80
|
+
// Display comparator for packages, matching the merchant app's PackageComparator (minus clauses
|
|
81
|
+
// a storefront can never exercise: disabled packages and the purchase price list, neither sent)
|
|
82
|
+
static comparePackages(left, right) {
|
|
83
|
+
const byName = Catalog.compareNames(left.name, right.name);
|
|
84
|
+
if (byName !== 0)
|
|
85
|
+
return byName;
|
|
86
|
+
const byPrice = Catalog.comparePrices(left.price, right.price);
|
|
87
|
+
if (byPrice !== 0)
|
|
88
|
+
return byPrice;
|
|
89
|
+
return Catalog.compareIds(left, right);
|
|
90
|
+
}
|
|
91
|
+
// Display comparator for items generally: case insensitive, numeric, id-tiebroken
|
|
92
|
+
static compareByName(left, right) {
|
|
93
|
+
const byName = Catalog.compareNames(left.name, right.name);
|
|
94
|
+
if (byName !== 0)
|
|
95
|
+
return byName;
|
|
96
|
+
return Catalog.compareIds(left, right);
|
|
97
|
+
}
|
|
98
|
+
static compareNames(left, right) {
|
|
99
|
+
return left.localeCompare(right, undefined, { numeric: true, sensitivity: "accent" });
|
|
100
|
+
}
|
|
101
|
+
static compareIds(left, right) {
|
|
102
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
103
|
+
}
|
|
104
|
+
static comparePrices(left, right) {
|
|
105
|
+
// A package with no price sorts after one with a price: it cannot be bought
|
|
106
|
+
if (left === null)
|
|
107
|
+
return right === null ? 0 : 1;
|
|
108
|
+
if (right === null)
|
|
109
|
+
return -1;
|
|
110
|
+
return left.offerPrice - right.offerPrice;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// A business whose catalog has not arrived yet
|
|
114
|
+
Catalog.EMPTY = new Catalog(Object.freeze([]), Object.freeze([]), null);
|
|
115
|
+
export default Catalog;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Constants } from "./generated/Constants";
|
|
2
|
+
export declare function buildPriceListId(priceList: number): bigint;
|
|
3
|
+
export declare const MERABILLS_ONLINE_PRICE_LIST_ID: bigint;
|
|
4
|
+
export declare const MAX_SCALED_AMOUNT: bigint;
|
|
5
|
+
export declare function amountFromScaledValue(scaledAmount: bigint): number;
|
|
6
|
+
export declare function amountToScaledValue(amount: number): bigint;
|
|
7
|
+
export declare function quantityFromScaledValue(scaledQuantity: number): number;
|
|
8
|
+
export declare function quantityToScaledValue(quantity: number): number;
|
|
9
|
+
export declare function taxRateFromScaledValue(scaledTaxRate: number): number;
|
|
10
|
+
export declare function taxRateToScaledValue(taxRate: number): number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import Ids from "./Ids";
|
|
3
|
+
// The protocol's limits and scaling factors, and the wire-to-UI value conversions.
|
|
4
|
+
// Limits are re-exported rather than redeclared, so a Java constant has only one place to be wrong.
|
|
5
|
+
export { Constants } from "./generated/Constants";
|
|
6
|
+
// The id of a built-in price list, from the low part Constants carries for it:
|
|
7
|
+
// DEFAULT_SALE_PRICE_LIST, DEFAULT_PURCHASE_PRICE_LIST, MERABILLS_ONLINE_PRICE_LIST,
|
|
8
|
+
// AMAZON_SELLER_PRICE_LIST, FACEBOOK_MARKETPLACE_PRICE_LIST or WHATSAPP_BUSINESS_PRICE_LIST. These
|
|
9
|
+
// are the six ids of Java PriceListIds, built the way it builds them: the low part is a constant
|
|
10
|
+
// of the protocol, the id is not, 64 bits not fitting a protobuf enum value.
|
|
11
|
+
export function buildPriceListId(priceList) {
|
|
12
|
+
return Ids.buildId(proto_Constants.PRICE_LIST_ENTITY_TYPE_ID, BigInt(priceList));
|
|
13
|
+
}
|
|
14
|
+
// The only price list this library reads. A business prices the same package in several lists -
|
|
15
|
+
// what it sells at over the counter, what a marketplace it also sells on charges - and only this
|
|
16
|
+
// one is its storefront's. A price in any other list is passed over; see ProtoMap.buildBusiness.
|
|
17
|
+
export const MERABILLS_ONLINE_PRICE_LIST_ID = buildPriceListId(proto_Constants.MERABILLS_ONLINE_PRICE_LIST);
|
|
18
|
+
// The largest amount the protocol can carry (Java Transaction.MAX_AMOUNT); kept here since it doesn't fit
|
|
19
|
+
// a protobuf enum value. Unscaled it's well below 2^53, so an amount is always safe as a number.
|
|
20
|
+
export const MAX_SCALED_AMOUNT = 17592186044415n;
|
|
21
|
+
// An amount, price or discount travels multiplied by Constants.AMOUNT_FACTOR, bounded by
|
|
22
|
+
// MAX_SCALED_AMOUNT, so it's safe as a number even though it arrives as an int64.
|
|
23
|
+
export function amountFromScaledValue(scaledAmount) {
|
|
24
|
+
return Number(scaledAmount) / proto_Constants.AMOUNT_FACTOR;
|
|
25
|
+
}
|
|
26
|
+
export function amountToScaledValue(amount) {
|
|
27
|
+
return BigInt(Math.round(amount * proto_Constants.AMOUNT_FACTOR));
|
|
28
|
+
}
|
|
29
|
+
// A quantity travels multiplied by Constants.QUANTITY_FACTOR, so 1.5 kg arrives as 1500
|
|
30
|
+
export function quantityFromScaledValue(scaledQuantity) {
|
|
31
|
+
return scaledQuantity / proto_Constants.QUANTITY_FACTOR;
|
|
32
|
+
}
|
|
33
|
+
export function quantityToScaledValue(quantity) {
|
|
34
|
+
return Math.round(quantity * proto_Constants.QUANTITY_FACTOR);
|
|
35
|
+
}
|
|
36
|
+
// A tax rate travels multiplied by Constants.TAX_RATE_FACTOR, so 18% arrives as 1800. What comes
|
|
37
|
+
// back is a percentage and not a fraction: 18, not 0.18.
|
|
38
|
+
export function taxRateFromScaledValue(scaledTaxRate) {
|
|
39
|
+
return scaledTaxRate / (proto_Constants.TAX_RATE_FACTOR / 100);
|
|
40
|
+
}
|
|
41
|
+
export function taxRateToScaledValue(taxRate) {
|
|
42
|
+
return Math.round(taxRate * (proto_Constants.TAX_RATE_FACTOR / 100));
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What each container of the local store is called: a business container prefixes Business.businessId,
|
|
2
|
+
// a profile container prefixes the subject id. The prefix keeps the two id spaces from ever colliding.
|
|
3
|
+
// One business, holding that business and everything in its business database
|
|
4
|
+
export function forBusiness(nameSuffix) {
|
|
5
|
+
return `${VERSION_PREFIX}${BUSINESS_PREFIX}${nameSuffix}`;
|
|
6
|
+
}
|
|
7
|
+
// One signed in subject, holding their profile database and the tokens that sign their requests.
|
|
8
|
+
// Several can coexist so signing back in after a sign-out is incremental rather than a fresh download.
|
|
9
|
+
export function forProfile(nameSuffix) {
|
|
10
|
+
return `${VERSION_PREFIX}${PROFILE_PREFIX}${nameSuffix}`;
|
|
11
|
+
}
|
|
12
|
+
// Every name carries the layout version of what is inside it, so that a change to how this library
|
|
13
|
+
// stores a container does not have to be migrated: the new version simply reads and writes names
|
|
14
|
+
// nothing has written before, and what the old one left behind is inert until it is deleted.
|
|
15
|
+
// Bump this whenever a stored blob stops meaning what it used to - a caller cannot be relied on to
|
|
16
|
+
// clear a store it did not know had changed, and a half-understood container is worse than no cache.
|
|
17
|
+
const VERSION_PREFIX = "V1.";
|
|
18
|
+
// Neither id space is this library's to constrain, so a subject id that happened to equal a business
|
|
19
|
+
// id would otherwise put a profile and a catalog in one container
|
|
20
|
+
const BUSINESS_PREFIX = "b.";
|
|
21
|
+
const PROFILE_PREFIX = "p.";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default interface Database {
|
|
2
|
+
getContainer(name: string): Promise<Container>;
|
|
3
|
+
getContainerNames(): Promise<ReadonlyArray<string>>;
|
|
4
|
+
deleteContainer(name: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface Container {
|
|
7
|
+
read(id: bigint): Promise<StoredBlob | null>;
|
|
8
|
+
readAll(): Promise<ReadonlyArray<StoredBlob>>;
|
|
9
|
+
write(blobs: ReadonlyArray<StoredBlob>, deletes: ReadonlyArray<bigint>): Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
export interface StoredBlob {
|
|
12
|
+
readonly id: bigint;
|
|
13
|
+
readonly eTag: bigint | null;
|
|
14
|
+
readonly bytes: Uint8Array;
|
|
15
|
+
}
|
package/dist/Database.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/Entity.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default abstract class Entity {
|
|
2
|
+
protected constructor(header: EntityHeader);
|
|
3
|
+
get id(): bigint;
|
|
4
|
+
get createdAt(): Date;
|
|
5
|
+
get updatedAt(): Date;
|
|
6
|
+
get updateNumber(): bigint;
|
|
7
|
+
get updateInfo(): bigint;
|
|
8
|
+
get entityType(): number;
|
|
9
|
+
get updateType(): number;
|
|
10
|
+
get isDeleted(): boolean;
|
|
11
|
+
get parent(): Entity | null;
|
|
12
|
+
protected set parent(value: Entity | null);
|
|
13
|
+
private static readonly UPDATE_TYPE_MASK;
|
|
14
|
+
private readonly _header;
|
|
15
|
+
private _parent;
|
|
16
|
+
}
|
|
17
|
+
export declare class EntityHeader {
|
|
18
|
+
readonly id: bigint;
|
|
19
|
+
readonly createdAt: bigint;
|
|
20
|
+
readonly updatedAt: bigint;
|
|
21
|
+
readonly updateNumber: bigint;
|
|
22
|
+
readonly updateInfo: bigint;
|
|
23
|
+
constructor(id: bigint, createdAt: bigint, updatedAt: bigint, updateNumber: bigint, updateInfo: bigint);
|
|
24
|
+
}
|
|
25
|
+
export declare class UpdatableEntity extends Entity {
|
|
26
|
+
static setParent(child: Entity, parent: Entity | null): void;
|
|
27
|
+
}
|
package/dist/Entity.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import Ids from "./Ids";
|
|
3
|
+
// Something the protocol syncs one at a time: an id, create/update times, an update number, and
|
|
4
|
+
// the database it was last updated in. Rebuilt from the wire rather than edited in place, so the
|
|
5
|
+
// only mutable field is parent, set once containment is rebuilt after a sync (nothing nests on the wire).
|
|
6
|
+
class Entity {
|
|
7
|
+
constructor(header) {
|
|
8
|
+
this._parent = null;
|
|
9
|
+
this._header = header;
|
|
10
|
+
}
|
|
11
|
+
get id() {
|
|
12
|
+
return this._header.id;
|
|
13
|
+
}
|
|
14
|
+
get createdAt() {
|
|
15
|
+
return new Date(Number(this._header.createdAt));
|
|
16
|
+
}
|
|
17
|
+
get updatedAt() {
|
|
18
|
+
return new Date(Number(this._header.updatedAt));
|
|
19
|
+
}
|
|
20
|
+
// Monotonically increasing, and only comparable within the same entity type and database. Basis of a sync cursor.
|
|
21
|
+
get updateNumber() {
|
|
22
|
+
return this._header.updateNumber;
|
|
23
|
+
}
|
|
24
|
+
// The database this entity was last updated in (Constants.MASTER_DB_ID = the server); sync out uses it
|
|
25
|
+
// to avoid echoing a store's own changes back. Business entities only - always ENTITY_ID_NULL on profile entities.
|
|
26
|
+
get updateInfo() {
|
|
27
|
+
return this._header.updateInfo;
|
|
28
|
+
}
|
|
29
|
+
// One of the Constants.*_ENTITY_TYPE_ID values, read from the id's most significant byte (not on the wire).
|
|
30
|
+
get entityType() {
|
|
31
|
+
return Ids.getEntityTypeId(this.id);
|
|
32
|
+
}
|
|
33
|
+
// One of Constants.UPDATE_TYPE_CREATED, _MODIFIED or _DELETED, held in the update number's last two bits.
|
|
34
|
+
get updateType() {
|
|
35
|
+
return Number(this._header.updateNumber & Entity.UPDATE_TYPE_MASK);
|
|
36
|
+
}
|
|
37
|
+
// A deleted entity is a tombstone: only the header fields are meaningful. A UI never reaches one.
|
|
38
|
+
get isDeleted() {
|
|
39
|
+
return this.updateType === proto_Constants.UPDATE_TYPE_DELETED;
|
|
40
|
+
}
|
|
41
|
+
// What contains this entity, once the sync layer has rebuilt containment (e.g. the Business of a
|
|
42
|
+
// Configuration). Null while being built, and on Business itself, which is the root.
|
|
43
|
+
get parent() {
|
|
44
|
+
return this._parent;
|
|
45
|
+
}
|
|
46
|
+
set parent(value) {
|
|
47
|
+
this._parent = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Entity.UPDATE_TYPE_MASK = BigInt(proto_Constants.UPDATE_TYPE_MASK);
|
|
51
|
+
export default Entity;
|
|
52
|
+
// The fields of the Entity message every entity carries, passed as one value since they're all
|
|
53
|
+
// bigint and would otherwise transpose silently at a call site (the protocol has moved one before).
|
|
54
|
+
export class EntityHeader {
|
|
55
|
+
constructor(id, createdAt, updatedAt, updateNumber, updateInfo) {
|
|
56
|
+
this.id = id;
|
|
57
|
+
this.createdAt = createdAt;
|
|
58
|
+
this.updatedAt = updatedAt;
|
|
59
|
+
this.updateNumber = updateNumber;
|
|
60
|
+
this.updateInfo = updateInfo;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Lets the sync layer set an entity's parent via casting, kept off the surface a UI sees.
|
|
64
|
+
// Static because the setter it reaches is protected.
|
|
65
|
+
export class UpdatableEntity extends Entity {
|
|
66
|
+
static setParent(child, parent) {
|
|
67
|
+
child.parent = parent;
|
|
68
|
+
}
|
|
69
|
+
}
|