@mohasinac/appkit 4.45.0 β 4.46.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/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/categories/live-counts.d.ts +41 -0
- package/dist/_internal/server/features/categories/live-counts.js +72 -0
- package/dist/_internal/server/features/checkout/expand-order-items.js +17 -0
- package/dist/_internal/server/features/orders/adapters.js +29 -0
- package/dist/_internal/server/features/parts/assembly.d.ts +126 -0
- package/dist/_internal/server/features/parts/assembly.js +246 -0
- package/dist/_internal/server/features/parts/data.d.ts +65 -0
- package/dist/_internal/server/features/parts/data.js +170 -0
- package/dist/_internal/server/features/parts/og.d.ts +34 -0
- package/dist/_internal/server/features/parts/og.js +66 -0
- package/dist/_internal/server/features/search/search-everything.d.ts +75 -0
- package/dist/_internal/server/features/search/search-everything.js +120 -0
- package/dist/_internal/server/features/search/search-sources.d.ts +17 -0
- package/dist/_internal/server/features/search/search-sources.js +138 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.d.ts +70 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +302 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +128 -19
- package/dist/_internal/server/jobs/core/jobRunners.js +12 -0
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +73 -3
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -43
- package/dist/_internal/server/jobs/core/productViewWindows.d.ts +2 -0
- package/dist/_internal/server/jobs/core/productViewWindows.js +173 -0
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +27 -1
- package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +20 -0
- package/dist/_internal/server/jobs/handlers/pageViewRollup.js +25 -1
- package/dist/_internal/shared/actions/action-registry.js +36 -0
- package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
- package/dist/_internal/shared/features/shipping/protections.d.ts +93 -1
- package/dist/_internal/shared/features/shipping/protections.js +127 -0
- package/dist/_internal/shared/search/part-aliases.d.ts +44 -0
- package/dist/_internal/shared/search/part-aliases.js +104 -0
- package/dist/constants/api-endpoints.d.ts +18 -12
- package/dist/constants/api-endpoints.js +6 -8
- package/dist/constants/field-names.d.ts +0 -2
- package/dist/constants/field-names.js +0 -2
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -2
- package/dist/errors/messages.d.ts +0 -7
- package/dist/errors/messages.js +0 -8
- package/dist/features/admin/components/AdminSectionsView.js +83 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +6 -2
- package/dist/features/admin/components/sections/sectionTemplates.d.ts +117 -0
- package/dist/features/admin/components/sections/sectionTemplates.js +213 -0
- package/dist/features/admin/schemas/firestore.d.ts +15 -0
- package/dist/features/admin/types/index.d.ts +0 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +0 -2
- package/dist/features/categories/api/[id]/description-template/route.js +40 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -3
- package/dist/features/categories/components/CategoryDetailPageView.js +89 -14
- package/dist/features/categories/components/CategoryTemplatePicker.js +9 -1
- package/dist/features/categories/repository/categories.repository.d.ts +65 -0
- package/dist/features/categories/repository/categories.repository.js +168 -22
- package/dist/features/categories/repository/category-counts.d.ts +143 -0
- package/dist/features/categories/repository/category-counts.js +224 -0
- package/dist/features/categories/schemas/category-counts.d.ts +155 -0
- package/dist/features/categories/schemas/category-counts.js +116 -0
- package/dist/features/categories/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/schemas/firestore.js +0 -1
- package/dist/features/categories/utils/breadcrumb-chain.d.ts +194 -0
- package/dist/features/categories/utils/breadcrumb-chain.js +202 -0
- package/dist/features/corporate/schemas/index.d.ts +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +13 -0
- package/dist/features/orders/types/index.d.ts +30 -0
- package/dist/features/parts/components/AssemblyDrawer.d.ts +107 -0
- package/dist/features/parts/components/AssemblyDrawer.js +250 -0
- package/dist/features/parts/components/ModelCompositionPanel.d.ts +37 -0
- package/dist/features/parts/components/ModelCompositionPanel.js +34 -0
- package/dist/features/parts/components/PartDetailPageView.d.ts +48 -0
- package/dist/features/parts/components/PartDetailPageView.js +20 -0
- package/dist/features/parts/components/PartsIndexPageView.d.ts +33 -0
- package/dist/features/parts/components/PartsIndexPageView.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +257 -0
- package/dist/features/parts/schemas/firestore.js +181 -0
- package/dist/features/parts/utils/colourway-match.d.ts +62 -0
- package/dist/features/parts/utils/colourway-match.js +98 -0
- package/dist/features/parts/utils/part-breakdown.d.ts +53 -0
- package/dist/features/parts/utils/part-breakdown.js +77 -0
- package/dist/features/products/components/ProductDetailPageView.d.ts +14 -1
- package/dist/features/products/components/ProductDetailPageView.js +22 -3
- package/dist/features/products/components/ProductTabsShell.d.ts +3 -1
- package/dist/features/products/components/ProductTabsShell.js +4 -1
- package/dist/features/products/repository/category-assignment.d.ts +132 -0
- package/dist/features/products/repository/category-assignment.js +304 -0
- package/dist/features/products/repository/products.repository.d.ts +58 -1
- package/dist/features/products/repository/products.repository.js +255 -28
- package/dist/features/products/schemas/firestore.d.ts +147 -6
- package/dist/features/products/schemas/firestore.js +49 -0
- package/dist/features/search/components/SearchResultsPageView.d.ts +33 -0
- package/dist/features/search/components/SearchResultsPageView.js +21 -0
- package/dist/features/seller/components/SellerProductsView.js +129 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -15
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +166 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +28 -1
- package/dist/index.d.ts +11 -16
- package/dist/index.js +26 -18
- package/dist/next/routing/route-map.d.ts +6 -2
- package/dist/next/routing/route-map.js +14 -1
- package/dist/providers/db-firebase/base.repository.d.ts +31 -0
- package/dist/providers/db-firebase/base.repository.js +31 -0
- package/dist/repositories/index.d.ts +1 -2
- package/dist/repositories/index.js +0 -1
- package/dist/seed/_helpers/category-content.d.ts +29 -0
- package/dist/seed/_helpers/category-content.js +98 -1
- package/dist/seed/_helpers/category-forest.js +104 -24
- package/dist/seed/_helpers/category-parts.d.ts +9 -0
- package/dist/seed/_helpers/category-parts.js +4594 -0
- package/dist/seed/carousel-slides-seed-data.js +81 -8
- package/dist/seed/categories-seed-data.js +44 -6
- package/dist/seed/homepage-sections-seed-data.js +170 -9
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +13 -0
- package/dist/seed/model-composition-seed-data.d.ts +15 -0
- package/dist/seed/model-composition-seed-data.js +8918 -0
- package/dist/seed/part-glossary-seed-data.d.ts +15 -0
- package/dist/seed/part-glossary-seed-data.js +10729 -0
- package/dist/seed/products-standard-seed-data.js +52 -0
- package/dist/seed/seed-purpose.d.ts +103 -4
- package/dist/seed/seed-purpose.js +162 -11
- package/dist/seo/json-ld.d.ts +10 -1
- package/dist/seo/json-ld.js +9 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +11 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/id-generators.d.ts +30 -0
- package/dist/utils/id-generators.js +31 -1
- package/dist/utils/search-txt-builders.js +22 -0
- package/dist/utils/search-txt.d.ts +22 -0
- package/dist/utils/search-txt.js +17 -0
- package/dist/values/success-messages.d.ts +0 -4
- package/dist/values/success-messages.js +0 -5
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +292 -16
- package/dist/core/copilot-log.repository.d.ts +0 -44
- package/dist/core/copilot-log.repository.js +0 -61
- package/dist/features/copilot/components/AdminCopilotView.d.ts +0 -21
- package/dist/features/copilot/components/AdminCopilotView.js +0 -51
- package/dist/features/copilot/components/index.d.ts +0 -2
- package/dist/features/copilot/components/index.js +0 -1
- package/dist/features/copilot/hooks/useCopilotChat.d.ts +0 -25
- package/dist/features/copilot/hooks/useCopilotChat.js +0 -65
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +0 -15
- package/dist/features/copilot/hooks/useCopilotFeedback.js +0 -15
- package/dist/features/copilot/index.d.ts +0 -3
- package/dist/features/copilot/index.js +0 -3
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.46.0",
|
|
4
|
+
"head": "34ce98d7f80659ca1052873c7444d69dde36c07b",
|
|
5
5
|
"branch": "main",
|
|
6
|
-
"dirty":
|
|
7
|
-
"builtAt": "2026-10-
|
|
6
|
+
"dirty": true,
|
|
7
|
+
"builtAt": "2026-10-13T00:19:07.494Z"
|
|
8
8
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G3b β read the live `categoryCounts` documents for a BOUNDED set of ids.
|
|
3
|
+
*
|
|
4
|
+
* π WHO THIS IS FOR, because it is not the category detail page.
|
|
5
|
+
* `CategoryDetailPageView` already computes its own tab counts live
|
|
6
|
+
* (`listing-tab-counts`), so its headline numbers were never stale. The stale
|
|
7
|
+
* readers are the ones that render MANY categories and therefore cannot query
|
|
8
|
+
* per category: the `/categories` grid, the homepage category tiles, the
|
|
9
|
+
* admin table, the tree, and the child chips on a category page. All five read
|
|
10
|
+
* `metrics.totalProductCount`, which only the nightly reconciler writes.
|
|
11
|
+
*
|
|
12
|
+
* π `db.getAll` β ONE round-trip for N ids, not N round-trips. That is what
|
|
13
|
+
* reconciles this with the plan's own warning that `CategoryDetailPageView` is
|
|
14
|
+
* already at Rule #6's ~3-sequential-round-trip ceiling and "a sidecar
|
|
15
|
+
* collection would add an eighth": the existing seven reads are a
|
|
16
|
+
* `Promise.all`, so an eighth PARALLEL read adds no latency. It does add N
|
|
17
|
+
* document reads of quota, bounded by the page size the caller already
|
|
18
|
+
* fetched.
|
|
19
|
+
*
|
|
20
|
+
* π A missing document is `undefined`, never 0. The caller must fall back to
|
|
21
|
+
* `metrics` β rendering a confident zero for a category whose counter has not
|
|
22
|
+
* been written yet is Root Cause #72's lesson (a count that is unknown must
|
|
23
|
+
* never be indistinguishable from a count of nothing), and it would make this
|
|
24
|
+
* change a regression on every category until its first mutation.
|
|
25
|
+
*/
|
|
26
|
+
export interface LiveCountsResult {
|
|
27
|
+
/** categoryId β the rolled-up total, when a counts document exists. */
|
|
28
|
+
total: Map<string, number>;
|
|
29
|
+
/** True when the id list was truncated, so the caller can stop claiming completeness. */
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function getLiveCategoryCounts(db: {
|
|
33
|
+
collection(name: string): {
|
|
34
|
+
doc(id: string): unknown;
|
|
35
|
+
};
|
|
36
|
+
getAll(...refs: unknown[]): Promise<Array<{
|
|
37
|
+
exists: boolean;
|
|
38
|
+
id: string;
|
|
39
|
+
data(): unknown;
|
|
40
|
+
}>>;
|
|
41
|
+
}, ids: readonly string[]): Promise<LiveCountsResult>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G3b β read the live `categoryCounts` documents for a BOUNDED set of ids.
|
|
3
|
+
*
|
|
4
|
+
* π WHO THIS IS FOR, because it is not the category detail page.
|
|
5
|
+
* `CategoryDetailPageView` already computes its own tab counts live
|
|
6
|
+
* (`listing-tab-counts`), so its headline numbers were never stale. The stale
|
|
7
|
+
* readers are the ones that render MANY categories and therefore cannot query
|
|
8
|
+
* per category: the `/categories` grid, the homepage category tiles, the
|
|
9
|
+
* admin table, the tree, and the child chips on a category page. All five read
|
|
10
|
+
* `metrics.totalProductCount`, which only the nightly reconciler writes.
|
|
11
|
+
*
|
|
12
|
+
* π `db.getAll` β ONE round-trip for N ids, not N round-trips. That is what
|
|
13
|
+
* reconciles this with the plan's own warning that `CategoryDetailPageView` is
|
|
14
|
+
* already at Rule #6's ~3-sequential-round-trip ceiling and "a sidecar
|
|
15
|
+
* collection would add an eighth": the existing seven reads are a
|
|
16
|
+
* `Promise.all`, so an eighth PARALLEL read adds no latency. It does add N
|
|
17
|
+
* document reads of quota, bounded by the page size the caller already
|
|
18
|
+
* fetched.
|
|
19
|
+
*
|
|
20
|
+
* π A missing document is `undefined`, never 0. The caller must fall back to
|
|
21
|
+
* `metrics` β rendering a confident zero for a category whose counter has not
|
|
22
|
+
* been written yet is Root Cause #72's lesson (a count that is unknown must
|
|
23
|
+
* never be indistinguishable from a count of nothing), and it would make this
|
|
24
|
+
* change a regression on every category until its first mutation.
|
|
25
|
+
*/
|
|
26
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
27
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
28
|
+
import { CATEGORY_COUNTS_COLLECTION, liveTotalFor, } from "../../../../features/categories/schemas/category-counts.js";
|
|
29
|
+
/** Hard ceiling on one batch β `getAll` is a single RPC but not a free one. */
|
|
30
|
+
const MAX_IDS = 200;
|
|
31
|
+
export async function getLiveCategoryCounts(db, ids) {
|
|
32
|
+
const unique = Array.from(new Set(ids.filter(Boolean)));
|
|
33
|
+
const truncated = unique.length > MAX_IDS;
|
|
34
|
+
const take = truncated ? unique.slice(0, MAX_IDS) : unique;
|
|
35
|
+
const total = new Map();
|
|
36
|
+
if (take.length === 0)
|
|
37
|
+
return { total, truncated };
|
|
38
|
+
try {
|
|
39
|
+
const col = db.collection(CATEGORY_COUNTS_COLLECTION);
|
|
40
|
+
const snaps = await db.getAll(...take.map((id) => col.doc(id)));
|
|
41
|
+
for (const snap of snaps) {
|
|
42
|
+
if (!snap.exists)
|
|
43
|
+
continue;
|
|
44
|
+
const v = liveTotalFor(snap.data());
|
|
45
|
+
if (v !== undefined)
|
|
46
|
+
total.set(snap.id, v);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// Typed FIRST, on the catch's own line, because `audit-catch-normalize`
|
|
51
|
+
// reads a short window after the clause and the explanation below is
|
|
52
|
+
// longer than it.
|
|
53
|
+
const e = normalizeError(err);
|
|
54
|
+
/*
|
|
55
|
+
* Fail OPEN with an empty map, so every caller falls back to `metrics` and
|
|
56
|
+
* renders exactly as it does today. A count is a display number: throwing
|
|
57
|
+
* here would take down a category page over a counter, which is the trade
|
|
58
|
+
* `.catch(() => null)` normally gets wrong β the difference is that here
|
|
59
|
+
* the fallback is a REAL alternative value, not an empty page.
|
|
60
|
+
*
|
|
61
|
+
* π But it is LOGGED, not swallowed. `void normalizeError(err)` as a
|
|
62
|
+
* catch's only statement is as silent as `catch {}` while looking handled,
|
|
63
|
+
* which is what `audit-silent-degrade` calls normalize theatre β and it
|
|
64
|
+
* caught exactly that here. Silent to the visitor, loud to the operator.
|
|
65
|
+
*/
|
|
66
|
+
serverLogger.warn("categoryCounts batch read failed β falling back to metrics", {
|
|
67
|
+
ids: take.length,
|
|
68
|
+
error: e.message,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { total, truncated };
|
|
72
|
+
}
|
|
@@ -72,6 +72,19 @@ export function expandCartLineToOrderRows(item, representative, productById) {
|
|
|
72
72
|
...(representative.categorySlugs?.[0]
|
|
73
73
|
? { categorySlug: representative.categorySlugs[0] }
|
|
74
74
|
: {}),
|
|
75
|
+
/*
|
|
76
|
+
* I8 β the category and brand DISPLAY NAMES, beside the slug.
|
|
77
|
+
*
|
|
78
|
+
* `categoryNames[0]` is the default leaf's name, parallel to
|
|
79
|
+
* `categorySlugs[0]`. Snapshotted for the reason the slug alone is not
|
|
80
|
+
* enough: a slug survives a rename, but a receipt has to show a NAME, and
|
|
81
|
+
* re-resolving it at render time would let a renamed category change what
|
|
82
|
+
* a past invoice says.
|
|
83
|
+
*/
|
|
84
|
+
...(representative.categoryNames?.[0]
|
|
85
|
+
? { categoryName: representative.categoryNames[0] }
|
|
86
|
+
: {}),
|
|
87
|
+
...(representative.brand ? { brand: representative.brand } : {}),
|
|
75
88
|
...(representative.listingType ? { listingType: representative.listingType } : {}),
|
|
76
89
|
...(representative.condition ? { condition: representative.condition } : {}),
|
|
77
90
|
// Legacy collapsed bundle rows (no groupMembers) keep their old shape so
|
|
@@ -122,6 +135,10 @@ export function expandCartLineToOrderRows(item, representative, productById) {
|
|
|
122
135
|
* been deleted, which is the whole reason that snapshot exists.
|
|
123
136
|
*/
|
|
124
137
|
...(product?.categorySlugs?.[0] ? { categorySlug: product.categorySlugs[0] } : {}),
|
|
138
|
+
// I8 - the same snapshot per member. `product` may be gone by order time,
|
|
139
|
+
// which is exactly why the member snapshot exists, so both are guarded.
|
|
140
|
+
...(product?.categoryNames?.[0] ? { categoryName: product.categoryNames[0] } : {}),
|
|
141
|
+
...(product?.brand ? { brand: product.brand } : {}),
|
|
125
142
|
...(product?.listingType ? { listingType: product.listingType } : {}),
|
|
126
143
|
// Per MEMBER β a bundle can mix a sealed item with a loose one, and a
|
|
127
144
|
// blended condition would describe neither.
|
|
@@ -66,6 +66,35 @@ export function orderDocumentToOrder(doc) {
|
|
|
66
66
|
// only Item / Qty / Price (Root Cause #57).
|
|
67
67
|
...(item.hsnCode ? { hsnCode: item.hsnCode } : {}),
|
|
68
68
|
...(item.gstRate != null ? { gstRate: item.gstRate } : {}),
|
|
69
|
+
/*
|
|
70
|
+
* I8 β the TAXONOMY snapshot, which this adapter dropped.
|
|
71
|
+
*
|
|
72
|
+
* π Root Cause #57's exact shape, for the THIRD time in this one
|
|
73
|
+
* mapper: the fields exist on the document, are deliberately
|
|
74
|
+
* snapshotted at order time, and never reached any client. `finalSale`
|
|
75
|
+
* and the tax pair above were each added after the same discovery.
|
|
76
|
+
*
|
|
77
|
+
* `categorySlug` has carried a reason since C1 β *"a product
|
|
78
|
+
* re-categorised next year must not retroactively move last year's sale
|
|
79
|
+
* into a band it was never part of"* β and no order surface could read
|
|
80
|
+
* it. `categoryName` and `brand` are new, because a slug survives a
|
|
81
|
+
* rename and a NAME is what a receipt has to show: the whole point of
|
|
82
|
+
* snapshotting is that the record still reads correctly after the
|
|
83
|
+
* category or product it came from is renamed or deleted.
|
|
84
|
+
*/
|
|
85
|
+
...(item.categorySlug ? { categorySlug: item.categorySlug } : {}),
|
|
86
|
+
...(item.categoryName ? { categoryName: item.categoryName } : {}),
|
|
87
|
+
...(item.brand ? { brand: item.brand } : {}),
|
|
88
|
+
...(item.condition ? { condition: item.condition } : {}),
|
|
89
|
+
/*
|
|
90
|
+
* The grouped-line keys. `groupOrderItemsByLine` collapses a bundle or
|
|
91
|
+
* grouped cart line for the receipt and keys on exactly these β so
|
|
92
|
+
* without them the client saw N separate rows where the buyer bought
|
|
93
|
+
* one thing.
|
|
94
|
+
*/
|
|
95
|
+
...(item.groupSlug ? { groupSlug: item.groupSlug } : {}),
|
|
96
|
+
...(item.groupTitle ? { groupTitle: item.groupTitle } : {}),
|
|
97
|
+
...(item.groupSource ? { groupSource: item.groupSource } : {}),
|
|
69
98
|
}))
|
|
70
99
|
: [
|
|
71
100
|
{
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** One component the assembly consumes. */
|
|
2
|
+
export interface AssemblyComponent {
|
|
3
|
+
slotKey: string;
|
|
4
|
+
/** A part LISTING's id β a `ProductDocument`, not a `partGlossary` row. */
|
|
5
|
+
productId: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AssembleInput {
|
|
9
|
+
/** The store doing the assembly. Parts are `storeId`-scoped like any listing. */
|
|
10
|
+
storeId: string;
|
|
11
|
+
/** The complete-bey listing being created or restocked. */
|
|
12
|
+
targetProductId: string;
|
|
13
|
+
components: readonly AssemblyComponent[];
|
|
14
|
+
/** How many complete beys to build. */
|
|
15
|
+
units?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AssemblyResult {
|
|
18
|
+
targetProductId: string;
|
|
19
|
+
unitsBuilt: number;
|
|
20
|
+
/** Per component: what it had available, and what it has now. */
|
|
21
|
+
consumed: Array<{
|
|
22
|
+
productId: string;
|
|
23
|
+
before: number;
|
|
24
|
+
after: number;
|
|
25
|
+
taken: number;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/** The hard cap, matching the write schema's `.max(8)` on `assembledFrom`. */
|
|
29
|
+
export declare const MAX_ASSEMBLY_COMPONENTS = 8;
|
|
30
|
+
/**
|
|
31
|
+
* π The fields read inside the transaction, named once.
|
|
32
|
+
*
|
|
33
|
+
* `availableQuantity` is the gate (see the header). `stockQuantity` is read
|
|
34
|
+
* only so the reservation can be CARRIED FORWARD rather than discarded β
|
|
35
|
+
* writing `availableQuantity` without adjusting `stockQuantity` by the same
|
|
36
|
+
* amount would make `reserved` drift by exactly the units consumed, and
|
|
37
|
+
* `resolveStockResync` would then hand those units back to the shelf on the
|
|
38
|
+
* seller's next edit. That is A4's bug arriving through a new door.
|
|
39
|
+
*/
|
|
40
|
+
interface StockSnapshot {
|
|
41
|
+
stockQuantity: number;
|
|
42
|
+
availableQuantity: number;
|
|
43
|
+
storeId?: string;
|
|
44
|
+
}
|
|
45
|
+
/** What a planner decided to write, before anything touches Firestore. */
|
|
46
|
+
export interface StockWrite {
|
|
47
|
+
productId: string;
|
|
48
|
+
stockQuantity: number;
|
|
49
|
+
availableQuantity: number;
|
|
50
|
+
}
|
|
51
|
+
export interface AssemblyPlan {
|
|
52
|
+
writes: StockWrite[];
|
|
53
|
+
consumed: AssemblyResult["consumed"];
|
|
54
|
+
/** The complete-bey listing's own new figures. */
|
|
55
|
+
target: StockWrite;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The whole decision, as a PURE function of the documents and the request.
|
|
59
|
+
*
|
|
60
|
+
* π Extracted because the arithmetic is the part most likely to be wrong and
|
|
61
|
+
* the transaction is the part hardest to test. A fake Firestore cannot be
|
|
62
|
+
* injected across an ESM namespace boundary β measured: `Object.defineProperty`
|
|
63
|
+
* on the imported namespace throws, so a mock-based harness reported every
|
|
64
|
+
* ordering assertion as green having executed none of them. Rather than
|
|
65
|
+
* weaken the module with a test-only injection seam, the thing worth asserting
|
|
66
|
+
* moved OUT of the transaction: which field gates the build, how the
|
|
67
|
+
* reservation is carried, and what is refused.
|
|
68
|
+
*
|
|
69
|
+
* `direction` is `-1` for an assembly (components are consumed) and `+1` for a
|
|
70
|
+
* part-out (components are produced), so there is ONE implementation of the
|
|
71
|
+
* gate and the carry rather than two that must agree.
|
|
72
|
+
*/
|
|
73
|
+
export declare function planAssembly(args: {
|
|
74
|
+
storeId: string;
|
|
75
|
+
/** The complete bey: consumed on part-out, produced on assembly. */
|
|
76
|
+
subject: {
|
|
77
|
+
productId: string;
|
|
78
|
+
doc: StockSnapshot | undefined;
|
|
79
|
+
};
|
|
80
|
+
components: ReadonlyArray<{
|
|
81
|
+
request: AssemblyComponent;
|
|
82
|
+
doc: StockSnapshot | undefined;
|
|
83
|
+
}>;
|
|
84
|
+
units: number;
|
|
85
|
+
/** -1 consumes the components (assemble); +1 produces them (part out). */
|
|
86
|
+
direction: -1 | 1;
|
|
87
|
+
}): AssemblyPlan;
|
|
88
|
+
/**
|
|
89
|
+
* The input rules, shared by both directions so they cannot drift.
|
|
90
|
+
*
|
|
91
|
+
* Runs before any Firestore access, which is also why it is separable.
|
|
92
|
+
*/
|
|
93
|
+
export declare function assertAssemblyInput(components: readonly AssemblyComponent[], units: number): void;
|
|
94
|
+
/**
|
|
95
|
+
* Build `units` copies of a complete bey, consuming its components.
|
|
96
|
+
*
|
|
97
|
+
* A thin shell over `planAssembly`: read everything, decide purely, write.
|
|
98
|
+
*
|
|
99
|
+
* Throws rather than partially succeeding β
|
|
100
|
+
* - `ValidationError` β malformed, missing, or another store's listing.
|
|
101
|
+
* - `ConflictError` β a component does not have enough available. 409 and
|
|
102
|
+
* not 400: the request is well formed and the seller is not confused, the
|
|
103
|
+
* world changed under them, so the next action is to restock rather than to
|
|
104
|
+
* fix a field. Same distinction as removing a booked lottery slot.
|
|
105
|
+
*/
|
|
106
|
+
export declare function assembleFromComponents(input: AssembleInput): Promise<AssemblyResult>;
|
|
107
|
+
export interface PartOutInput {
|
|
108
|
+
storeId: string;
|
|
109
|
+
/** The complete bey being broken down. */
|
|
110
|
+
sourceProductId: string;
|
|
111
|
+
/** Which component listings receive the parts. */
|
|
112
|
+
components: readonly AssemblyComponent[];
|
|
113
|
+
units?: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The inverse: decrement the bey, increment each component.
|
|
117
|
+
*
|
|
118
|
+
* π Spares arrive as whole beys too, so this is not a convenience β it is half
|
|
119
|
+
* the workflow. Without it, "part out" would only work on beys this system
|
|
120
|
+
* built, which is none of the current catalogue (GAP-10).
|
|
121
|
+
*
|
|
122
|
+
* Same planner, `direction: 1`. ONE implementation of the gate and the
|
|
123
|
+
* reservation carry rather than two that must agree.
|
|
124
|
+
*/
|
|
125
|
+
export declare function partOutToComponents(input: PartOutInput): Promise<AssemblyResult>;
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { unitOfWork } from "../../../../core/unit-of-work.js";
|
|
2
|
+
import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore.js";
|
|
3
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
4
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
5
|
+
import { ValidationError, ConflictError } from "../../../../errors/index.js";
|
|
6
|
+
/** The hard cap, matching the write schema's `.max(8)` on `assembledFrom`. */
|
|
7
|
+
export const MAX_ASSEMBLY_COMPONENTS = 8;
|
|
8
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
9
|
+
/**
|
|
10
|
+
* The whole decision, as a PURE function of the documents and the request.
|
|
11
|
+
*
|
|
12
|
+
* π Extracted because the arithmetic is the part most likely to be wrong and
|
|
13
|
+
* the transaction is the part hardest to test. A fake Firestore cannot be
|
|
14
|
+
* injected across an ESM namespace boundary β measured: `Object.defineProperty`
|
|
15
|
+
* on the imported namespace throws, so a mock-based harness reported every
|
|
16
|
+
* ordering assertion as green having executed none of them. Rather than
|
|
17
|
+
* weaken the module with a test-only injection seam, the thing worth asserting
|
|
18
|
+
* moved OUT of the transaction: which field gates the build, how the
|
|
19
|
+
* reservation is carried, and what is refused.
|
|
20
|
+
*
|
|
21
|
+
* `direction` is `-1` for an assembly (components are consumed) and `+1` for a
|
|
22
|
+
* part-out (components are produced), so there is ONE implementation of the
|
|
23
|
+
* gate and the carry rather than two that must agree.
|
|
24
|
+
*/
|
|
25
|
+
export function planAssembly(args) {
|
|
26
|
+
const { storeId, subject, components, units, direction } = args;
|
|
27
|
+
if (!subject.doc) {
|
|
28
|
+
throw new ValidationError(direction === -1
|
|
29
|
+
? "The listing being assembled no longer exists."
|
|
30
|
+
: "The listing being parted out no longer exists.");
|
|
31
|
+
}
|
|
32
|
+
if (subject.doc.storeId && subject.doc.storeId !== storeId) {
|
|
33
|
+
throw new ValidationError("That listing belongs to another store.");
|
|
34
|
+
}
|
|
35
|
+
/*
|
|
36
|
+
* π On a PART-OUT the subject is what gets consumed, so it needs the same
|
|
37
|
+
* availability gate as a component: a bey with 2 in stock and 1 reserved can
|
|
38
|
+
* only have ONE parted out. On an assembly the subject only ever gains, so
|
|
39
|
+
* there is nothing to check.
|
|
40
|
+
*/
|
|
41
|
+
const subjectAvailable = num(subject.doc.availableQuantity);
|
|
42
|
+
if (direction === 1 && subjectAvailable < units) {
|
|
43
|
+
throw new ConflictError(`Not enough available to part out: needs ${units}, has ${subjectAvailable}.`);
|
|
44
|
+
}
|
|
45
|
+
const consumed = [];
|
|
46
|
+
const writes = [];
|
|
47
|
+
for (const { request, doc } of components) {
|
|
48
|
+
if (!doc) {
|
|
49
|
+
throw new ValidationError(direction === -1
|
|
50
|
+
? `A component is missing: ${request.productId}`
|
|
51
|
+
: `A destination component is missing: ${request.productId}`);
|
|
52
|
+
}
|
|
53
|
+
/*
|
|
54
|
+
* π Scoped to the caller's own store. An admin assembling from a seller's
|
|
55
|
+
* bins would be writing another store's inventory; the only legitimate
|
|
56
|
+
* version is `store-letitrip-official` using its OWN stock, which this
|
|
57
|
+
* already allows.
|
|
58
|
+
*/
|
|
59
|
+
if (doc.storeId && doc.storeId !== storeId) {
|
|
60
|
+
throw new ValidationError(`A component belongs to another store: ${request.productId}`);
|
|
61
|
+
}
|
|
62
|
+
const delta = request.quantity * units * direction;
|
|
63
|
+
const available = num(doc.availableQuantity);
|
|
64
|
+
// π THE GATE, and it reads `availableQuantity` β see the file header.
|
|
65
|
+
if (direction === -1 && available < -delta) {
|
|
66
|
+
// 409, not 400: the request is well formed and the world changed.
|
|
67
|
+
// Figures in `data`, words in the client β see the part-out branch above.
|
|
68
|
+
throw new ConflictError(`component insufficient: needs ${-delta}, has ${available}`, "ASSEMBLY_INSUFFICIENT", { kind: "component", productId: request.productId, needs: -delta, has: available });
|
|
69
|
+
}
|
|
70
|
+
consumed.push({
|
|
71
|
+
productId: request.productId,
|
|
72
|
+
before: available,
|
|
73
|
+
after: available + delta,
|
|
74
|
+
taken: -delta,
|
|
75
|
+
});
|
|
76
|
+
writes.push({
|
|
77
|
+
productId: request.productId,
|
|
78
|
+
// Both fields move by the SAME amount, so an outstanding reservation is
|
|
79
|
+
// carried forward exactly rather than handed back to the shelf.
|
|
80
|
+
stockQuantity: Math.max(0, num(doc.stockQuantity) + delta),
|
|
81
|
+
availableQuantity: available + delta,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const subjectDelta = units * -direction;
|
|
85
|
+
return {
|
|
86
|
+
writes,
|
|
87
|
+
consumed,
|
|
88
|
+
target: {
|
|
89
|
+
productId: subject.productId,
|
|
90
|
+
stockQuantity: Math.max(0, num(subject.doc.stockQuantity) + subjectDelta),
|
|
91
|
+
availableQuantity: subjectAvailable + subjectDelta,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The input rules, shared by both directions so they cannot drift.
|
|
97
|
+
*
|
|
98
|
+
* Runs before any Firestore access, which is also why it is separable.
|
|
99
|
+
*/
|
|
100
|
+
export function assertAssemblyInput(components, units) {
|
|
101
|
+
if (!Number.isInteger(units) || units < 1 || units > 99) {
|
|
102
|
+
throw new ValidationError("Quantity must be a whole number between 1 and 99.");
|
|
103
|
+
}
|
|
104
|
+
if (components.length === 0) {
|
|
105
|
+
throw new ValidationError("An assembly needs at least one component.");
|
|
106
|
+
}
|
|
107
|
+
if (components.length > MAX_ASSEMBLY_COMPONENTS) {
|
|
108
|
+
throw new ValidationError(`An assembly may involve at most ${MAX_ASSEMBLY_COMPONENTS} components.`);
|
|
109
|
+
}
|
|
110
|
+
/*
|
|
111
|
+
* π Two entries for one product id would each be planned against the SAME
|
|
112
|
+
* read value and each subtract from it, so the second would silently
|
|
113
|
+
* overdraw. Refused rather than summed: a caller sending a product twice is
|
|
114
|
+
* describing something it does not mean, and summing would decide for them.
|
|
115
|
+
*/
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const c of components) {
|
|
118
|
+
if (seen.has(c.productId)) {
|
|
119
|
+
throw new ValidationError("The same component appears twice. Combine it into one row with the total quantity.");
|
|
120
|
+
}
|
|
121
|
+
seen.add(c.productId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build `units` copies of a complete bey, consuming its components.
|
|
126
|
+
*
|
|
127
|
+
* A thin shell over `planAssembly`: read everything, decide purely, write.
|
|
128
|
+
*
|
|
129
|
+
* Throws rather than partially succeeding β
|
|
130
|
+
* - `ValidationError` β malformed, missing, or another store's listing.
|
|
131
|
+
* - `ConflictError` β a component does not have enough available. 409 and
|
|
132
|
+
* not 400: the request is well formed and the seller is not confused, the
|
|
133
|
+
* world changed under them, so the next action is to restock rather than to
|
|
134
|
+
* fix a field. Same distinction as removing a booked lottery slot.
|
|
135
|
+
*/
|
|
136
|
+
export async function assembleFromComponents(input) {
|
|
137
|
+
const units = input.units ?? 1;
|
|
138
|
+
assertAssemblyInput(input.components, units);
|
|
139
|
+
const col = getAdminDb().collection(PRODUCT_COLLECTION);
|
|
140
|
+
return unitOfWork.runTransaction(async (tx) => {
|
|
141
|
+
// ββ EVERY READ FIRST. Firestore requires it, and so does correctness. ββ
|
|
142
|
+
const subjectRef = col.doc(input.targetProductId);
|
|
143
|
+
const componentRefs = input.components.map((c) => col.doc(c.productId));
|
|
144
|
+
const [subjectSnap, ...componentSnaps] = await tx.getAll(subjectRef, ...componentRefs);
|
|
145
|
+
const plan = planAssembly({
|
|
146
|
+
storeId: input.storeId,
|
|
147
|
+
subject: {
|
|
148
|
+
productId: input.targetProductId,
|
|
149
|
+
doc: subjectSnap.exists ? subjectSnap.data() : undefined,
|
|
150
|
+
},
|
|
151
|
+
components: input.components.map((request, i) => ({
|
|
152
|
+
request,
|
|
153
|
+
doc: componentSnaps[i].exists
|
|
154
|
+
? componentSnaps[i].data()
|
|
155
|
+
: undefined,
|
|
156
|
+
})),
|
|
157
|
+
units,
|
|
158
|
+
direction: -1,
|
|
159
|
+
});
|
|
160
|
+
// ββ WRITES ONLY AFTER EVERY READ ββββββββββββββββββββββββββββββββββββββ
|
|
161
|
+
const now = new Date();
|
|
162
|
+
for (const w of plan.writes) {
|
|
163
|
+
tx.update(col.doc(w.productId), {
|
|
164
|
+
stockQuantity: w.stockQuantity,
|
|
165
|
+
availableQuantity: w.availableQuantity,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
tx.update(subjectRef, {
|
|
170
|
+
stockQuantity: plan.target.stockQuantity,
|
|
171
|
+
availableQuantity: plan.target.availableQuantity,
|
|
172
|
+
assembledFrom: input.components.map((c) => ({ ...c })),
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
});
|
|
175
|
+
serverLogger.info("assembleFromComponents", {
|
|
176
|
+
storeId: input.storeId,
|
|
177
|
+
targetProductId: input.targetProductId,
|
|
178
|
+
units,
|
|
179
|
+
components: plan.consumed.length,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
targetProductId: input.targetProductId,
|
|
183
|
+
unitsBuilt: units,
|
|
184
|
+
consumed: plan.consumed,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The inverse: decrement the bey, increment each component.
|
|
190
|
+
*
|
|
191
|
+
* π Spares arrive as whole beys too, so this is not a convenience β it is half
|
|
192
|
+
* the workflow. Without it, "part out" would only work on beys this system
|
|
193
|
+
* built, which is none of the current catalogue (GAP-10).
|
|
194
|
+
*
|
|
195
|
+
* Same planner, `direction: 1`. ONE implementation of the gate and the
|
|
196
|
+
* reservation carry rather than two that must agree.
|
|
197
|
+
*/
|
|
198
|
+
export async function partOutToComponents(input) {
|
|
199
|
+
const units = input.units ?? 1;
|
|
200
|
+
assertAssemblyInput(input.components, units);
|
|
201
|
+
const col = getAdminDb().collection(PRODUCT_COLLECTION);
|
|
202
|
+
return unitOfWork.runTransaction(async (tx) => {
|
|
203
|
+
const subjectRef = col.doc(input.sourceProductId);
|
|
204
|
+
const componentRefs = input.components.map((c) => col.doc(c.productId));
|
|
205
|
+
const [subjectSnap, ...componentSnaps] = await tx.getAll(subjectRef, ...componentRefs);
|
|
206
|
+
const plan = planAssembly({
|
|
207
|
+
storeId: input.storeId,
|
|
208
|
+
subject: {
|
|
209
|
+
productId: input.sourceProductId,
|
|
210
|
+
doc: subjectSnap.exists ? subjectSnap.data() : undefined,
|
|
211
|
+
},
|
|
212
|
+
components: input.components.map((request, i) => ({
|
|
213
|
+
request,
|
|
214
|
+
doc: componentSnaps[i].exists
|
|
215
|
+
? componentSnaps[i].data()
|
|
216
|
+
: undefined,
|
|
217
|
+
})),
|
|
218
|
+
units,
|
|
219
|
+
direction: 1,
|
|
220
|
+
});
|
|
221
|
+
const now = new Date();
|
|
222
|
+
for (const w of plan.writes) {
|
|
223
|
+
tx.update(col.doc(w.productId), {
|
|
224
|
+
stockQuantity: w.stockQuantity,
|
|
225
|
+
availableQuantity: w.availableQuantity,
|
|
226
|
+
updatedAt: now,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
tx.update(subjectRef, {
|
|
230
|
+
stockQuantity: plan.target.stockQuantity,
|
|
231
|
+
availableQuantity: plan.target.availableQuantity,
|
|
232
|
+
updatedAt: now,
|
|
233
|
+
});
|
|
234
|
+
serverLogger.info("partOutToComponents", {
|
|
235
|
+
storeId: input.storeId,
|
|
236
|
+
sourceProductId: input.sourceProductId,
|
|
237
|
+
units,
|
|
238
|
+
components: plan.consumed.length,
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
targetProductId: input.sourceProductId,
|
|
242
|
+
unitsBuilt: units,
|
|
243
|
+
consumed: plan.consumed,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type PartGlossaryDocument, type ModelCompositionDocument, type PartSlot } from "../../../../features/parts/schemas/firestore";
|
|
2
|
+
/** The bucket cap. 87 is the largest real classification, so this clears it. */
|
|
3
|
+
export declare const PART_SLOT_PAGE_SIZE = 120;
|
|
4
|
+
/** One part, by `(slot, code|name)` β the only key this collection has. */
|
|
5
|
+
export declare const getPartByKey: (slot: PartSlot, codeOrName: string) => Promise<PartGlossaryDocument | null>;
|
|
6
|
+
/**
|
|
7
|
+
* The models that use a part, hydrated.
|
|
8
|
+
*
|
|
9
|
+
* π `getAll` over the ids the part ALREADY carries, not a query. `usedBy` is
|
|
10
|
+
* written by the generator, so this is one round trip for N ids with no index
|
|
11
|
+
* and no scan β and it is bounded by how many models actually use the part
|
|
12
|
+
* (measured max: 20).
|
|
13
|
+
*/
|
|
14
|
+
export declare const getModelsUsingPart: (partId: string) => Promise<ModelCompositionDocument[]>;
|
|
15
|
+
/** One model's composition, by the corpus doc id. */
|
|
16
|
+
export declare const getCompositionById: (id: string) => Promise<ModelCompositionDocument | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Hydrate the glossary rows a composition's slots point at, so a model page can
|
|
19
|
+
* link each slot and show its weight.
|
|
20
|
+
*
|
|
21
|
+
* ONE `getAll` for every slot β a per-slot read would be five round trips on a
|
|
22
|
+
* page that already has its own.
|
|
23
|
+
*/
|
|
24
|
+
export declare const getPartsForComposition: (compositionId: string) => Promise<Map<string, PartGlossaryDocument>>;
|
|
25
|
+
/**
|
|
26
|
+
* Compositions for a named-model CATEGORY.
|
|
27
|
+
*
|
|
28
|
+
* π ONE INDEXED EQUALITY on `categoryId`, joined at SEED-GENERATION time.
|
|
29
|
+
*
|
|
30
|
+
* The first version queried `searchTxt` on `modelComposition` β a field that
|
|
31
|
+
* collection does not have, so it would have matched NOTHING while looking
|
|
32
|
+
* like a working read. The two id spaces genuinely disagree
|
|
33
|
+
* (`gen1-plastic-dragoon-f` vs `category-dragoon-f`), so a join is required;
|
|
34
|
+
* doing it in the generator makes it deterministic, reviewable in the
|
|
35
|
+
* committed seed, and one equality here instead of a name query.
|
|
36
|
+
*
|
|
37
|
+
* Measured: **210 of 259** compositions join. The join is on the normalised
|
|
38
|
+
* name, and on the name with its parenthetical expansion removed β the corpus
|
|
39
|
+
* titles HMS beys `Aero Knight MS (Aero Knight Metal Sky)` where the category
|
|
40
|
+
* is "Aero Knight MS", and without that second form only 150 matched.
|
|
41
|
+
*
|
|
42
|
+
* π A non-match leaves `categoryId` unset and this returns `[]`, so the model
|
|
43
|
+
* page shows no composition. That is honest: a fuzzy match could attach
|
|
44
|
+
* ANOTHER bey's parts, and a wrong part list is worse than none. Reconciling
|
|
45
|
+
* the remaining 49 is E3's job.
|
|
46
|
+
*/
|
|
47
|
+
export declare const getCompositionsForCategory: (categoryId: string) => Promise<ModelCompositionDocument[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Every part in one classification, alphabetical.
|
|
50
|
+
*
|
|
51
|
+
* π ONE equality on `slot`, bounded. The biggest bucket is 87 rows (attack
|
|
52
|
+
* rings) and the smallest 4 (armour) β measured, not assumed β so a page size
|
|
53
|
+
* of 120 shows every one of them today without a pager, and `truncated` tells
|
|
54
|
+
* the view to stop claiming a total the moment that stops being true.
|
|
55
|
+
*
|
|
56
|
+
* Firestore needs no composite index for one equality plus an `orderBy` on
|
|
57
|
+
* the SAME field it is already keyed by document id within⦠except it does
|
|
58
|
+
* for `(slot ==, name asc)`. So the sort is applied IN MEMORY: 87 rows is
|
|
59
|
+
* nothing to sort, and a declared index that exists only to order a bucket
|
|
60
|
+
* this small is an index to deploy, wait on, and remember.
|
|
61
|
+
*/
|
|
62
|
+
export declare const listPartsBySlot: (slot: PartSlot) => Promise<{
|
|
63
|
+
parts: PartGlossaryDocument[];
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
}>;
|