@mohasinac/appkit 4.30.7 → 4.30.8

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.
@@ -184,6 +184,8 @@ export declare const PRODUCT_FIELDS: {
184
184
  };
185
185
  export declare const PRODUCT_STATUS_TRANSITIONS: Record<string, readonly string[]>;
186
186
  export declare const ORDER_FIELDS: {
187
+ /** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
188
+ readonly SEARCH_TXT: "searchTxt";
187
189
  readonly ID: "id";
188
190
  readonly PRODUCT_ID: "productId";
189
191
  readonly PRODUCT_TITLE: "productTitle";
@@ -210,6 +210,8 @@ export const PRODUCT_STATUS_TRANSITIONS = {
210
210
  // ORDER FIELDS
211
211
  // ============================================================================
212
212
  export const ORDER_FIELDS = {
213
+ /** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
214
+ SEARCH_TXT: "searchTxt",
213
215
  ID: "id",
214
216
  PRODUCT_ID: "productId",
215
217
  PRODUCT_TITLE: "productTitle",
@@ -85,7 +85,17 @@ export function AdminOrdersView({ children, ...props }) {
85
85
  const config = {
86
86
  portal: "admin",
87
87
  title: "Orders",
88
- // Search intentionally absent: this endpoint does not read `q`, so the box accepted typing and changed nothing. Restore it when the collection gains searchTxt — audit-listing-search-capability tracks it.
88
+ // Restored: `orders` has searchTxt and `/api/admin/orders` reads `q`.
89
+ //
90
+ // Matches the product, the store and the tracking number — deliberately NOT
91
+ // the buyer's name or email. Those are ORDER_PII_FIELDS, encrypted at rest,
92
+ // and a searchTxt for a name IS that name re-encoded (D1). The placeholder
93
+ // says what it matches so it does not promise the buyer lookup it cannot do.
94
+ search: {
95
+ placeholder: "Product, store, or tracking number",
96
+ mode: "partial",
97
+ fields: ["productTitle", "storeName", "trackingNumber"],
98
+ },
89
99
  emptyLabel: "No orders found",
90
100
  filterKeys: ["status", "paymentReview"],
91
101
  defaultSort: sortBy("createdAt", "DESC"),
@@ -1,3 +1,4 @@
1
+ import type { JsonValue } from "../../../schemas/types";
1
2
  import type { DocumentReference, WriteBatch } from "firebase-admin/firestore";
2
3
  import type { FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
4
  import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
@@ -153,6 +154,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
153
154
  listForSeller(productIds: string[], model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
154
155
  listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
155
156
  static readonly ADMIN_SIEVE_FIELDS: {
157
+ searchTxt: {
158
+ canFilter: boolean;
159
+ canSort: boolean;
160
+ };
156
161
  id: {
157
162
  canFilter: boolean;
158
163
  canSort: boolean;
@@ -244,7 +249,16 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
244
249
  canSort: boolean;
245
250
  };
246
251
  };
247
- listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
252
+ /**
253
+ * Derived on every write path via `applyWriteHooks`.
254
+ *
255
+ * Fed by product/store/tracking only — the three ORDER_PII_FIELDS and the
256
+ * shipping address are deliberately absent (see buildOrderSearchTxt).
257
+ */
258
+ protected buildSearchTxtFor(data: Record<string, JsonValue>): string[] | null;
259
+ listAll(model: SieveModel, opts?: {
260
+ search?: string;
261
+ }): Promise<FirebaseSieveResult<OrderDocument>>;
248
262
  /**
249
263
  * Append one refund event to the order and mark it non-contestable.
250
264
  * If `becomeRefunded` is true, also transitions the order status to "refunded".
@@ -1,3 +1,5 @@
1
+ import { buildOrderSearchTxt } from "../../../utils/search-txt-builders";
2
+ import { planSearchTxt, refineSearchTxt, emptySearchResult, } from "../../../utils/search-txt-query";
1
3
  import { normalizeError } from "../../../errors/normalize";
2
4
  import { DatabaseError, NotFoundError } from "../../../errors";
3
5
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
@@ -286,11 +288,30 @@ class OrderRepository extends BaseRepository {
286
288
  maxPageSize: 200,
287
289
  });
288
290
  }
289
- async listAll(model) {
290
- return this.sieveQuery(model, OrderRepository.ADMIN_SIEVE_FIELDS, {
291
+ /**
292
+ * Derived on every write path via `applyWriteHooks`.
293
+ *
294
+ * Fed by product/store/tracking only — the three ORDER_PII_FIELDS and the
295
+ * shipping address are deliberately absent (see buildOrderSearchTxt).
296
+ */
297
+ buildSearchTxtFor(data) {
298
+ return buildOrderSearchTxt(data);
299
+ }
300
+ async listAll(model, opts) {
301
+ const plan = planSearchTxt(opts?.search);
302
+ // A search that yielded no usable term must return NOTHING, not everything.
303
+ if (plan.empty)
304
+ return emptySearchResult();
305
+ let baseQuery = this.getCollection();
306
+ if (plan.head) {
307
+ baseQuery = baseQuery.where(ORDER_FIELDS.SEARCH_TXT, "array-contains", plan.head);
308
+ }
309
+ const result = await this.sieveQuery(model, OrderRepository.ADMIN_SIEVE_FIELDS, {
310
+ baseQuery,
291
311
  defaultPageSize: 50,
292
312
  maxPageSize: 200,
293
313
  });
314
+ return refineSearchTxt(result, plan.rest);
294
315
  }
295
316
  /**
296
317
  * Append one refund event to the order and mark it non-contestable.
@@ -604,6 +625,7 @@ OrderRepository.SELLER_SIEVE_FIELDS = {
604
625
  couponCode: { canFilter: true, canSort: false },
605
626
  };
606
627
  OrderRepository.ADMIN_SIEVE_FIELDS = {
628
+ searchTxt: { canFilter: true, canSort: false },
607
629
  id: { canFilter: true, canSort: false },
608
630
  userId: { canFilter: true, canSort: false },
609
631
  userName: { canFilter: true, canSort: true },
@@ -240,6 +240,8 @@ export interface AppliedOrderDiscount {
240
240
  storeId?: string;
241
241
  }
242
242
  export interface OrderDocument extends BaseDocument {
243
+ /** Word-prefix search tokens. Derived on write; never fed by a PII field. */
244
+ searchTxt?: string[];
243
245
  productId: string;
244
246
  productTitle: string;
245
247
  userId: string;
@@ -522,7 +524,7 @@ export interface OrderPaymentRecord {
522
524
  };
523
525
  }
524
526
  export declare const ORDER_COLLECTION: "orders";
525
- export declare const ORDER_INDEXED_FIELDS: readonly ["userId", "productId", "storeId", "status", "paymentStatus", "payoutStatus", "shippingMethod", "orderDate", "createdAt", "assignedWorkerId", "pickedAt", "packedAt"];
527
+ export declare const ORDER_INDEXED_FIELDS: readonly ["searchTxt", "userId", "productId", "storeId", "status", "paymentStatus", "payoutStatus", "shippingMethod", "orderDate", "createdAt", "assignedWorkerId", "pickedAt", "packedAt"];
526
528
  export declare const DEFAULT_ORDER_DATA: Partial<OrderDocument>;
527
529
  export declare const ORDER_PUBLIC_FIELDS: readonly ["id", "productId", "productTitle", "quantity", "unitPrice", "totalPrice", "currency", "status", "paymentStatus", "shippingAddress", "trackingNumber", "notes", "orderDate", "shippingDate", "deliveryDate", "createdAt"];
528
530
  export declare const ORDER_UPDATABLE_FIELDS: readonly ["notes", "shippingAddress"];
@@ -51,6 +51,7 @@ export const RefundStatusValues = {
51
51
  };
52
52
  export const ORDER_COLLECTION = "orders";
53
53
  export const ORDER_INDEXED_FIELDS = [
54
+ "searchTxt",
54
55
  "userId",
55
56
  "productId",
56
57
  "storeId",
@@ -2,7 +2,9 @@ import type { StoreDocument } from "../../features/stores/schemas/firestore";
2
2
  import type { EventDocument } from "../../features/events/schemas/firestore";
3
3
  import type { BlogPostDocument } from "../../features/blog/schemas/firestore";
4
4
  import type { ReviewDocument } from "../../features/reviews/schemas/firestore";
5
+ import type { OrderDocument } from "../../features/orders/schemas/firestore";
5
6
  export declare function withStoreSearchTxt<T extends Partial<StoreDocument>>(p: T): T;
6
7
  export declare function withEventSearchTxt<T extends Partial<EventDocument>>(p: T): T;
7
8
  export declare function withBlogSearchTxt<T extends Partial<BlogPostDocument>>(p: T): T;
8
9
  export declare function withReviewSearchTxt<T extends Partial<ReviewDocument>>(p: T): T;
10
+ export declare function withOrderSearchTxt<T extends Partial<OrderDocument>>(p: T): T;
@@ -1,4 +1,4 @@
1
- import { buildStoreSearchTxt, buildEventSearchTxt, buildBlogSearchTxt, buildReviewSearchTxt, } from "../../utils/search-txt-builders";
1
+ import { buildStoreSearchTxt, buildEventSearchTxt, buildBlogSearchTxt, buildReviewSearchTxt, buildOrderSearchTxt, } from "../../utils/search-txt-builders";
2
2
  export function withStoreSearchTxt(p) {
3
3
  return { ...p, searchTxt: buildStoreSearchTxt(p) };
4
4
  }
@@ -11,3 +11,6 @@ export function withBlogSearchTxt(p) {
11
11
  export function withReviewSearchTxt(p) {
12
12
  return { ...p, searchTxt: buildReviewSearchTxt(p) };
13
13
  }
14
+ export function withOrderSearchTxt(p) {
15
+ return { ...p, searchTxt: buildOrderSearchTxt(p) };
16
+ }
@@ -36,6 +36,7 @@
36
36
  * @tag consumers:seed/index.ts,seed/runner.ts
37
37
  * @tag sideEffects:none
38
38
  */
39
+ import { withOrderSearchTxt } from "./_helpers/search-txt-wrappers";
39
40
  import { seedExtMedia } from "./_helpers/media";
40
41
  const NOW = new Date();
41
42
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
@@ -723,4 +724,6 @@ export const ordersSeedData = [
723
724
  // it used to be 48 for the two manual-payment orders, and now also absorbs
724
725
  // the six acquisition-path orders.
725
726
  ...[..._rawOrdersSeedData, ...expandedOrders].slice(0, 42),
726
- ].map(withOrderImages);
727
+ // Derived through the wrapper, never written per record — an inline literal
728
+ // is how five product seed files shipped their last fixture with no tokens.
729
+ ].map(withOrderImages).map(withOrderSearchTxt);
@@ -3,6 +3,7 @@ import type { StoreDocument } from "../features/stores/schemas/firestore";
3
3
  import type { EventDocument } from "../features/events/schemas/firestore";
4
4
  import type { BlogPostDocument } from "../features/blog/schemas/firestore";
5
5
  import type { ReviewDocument } from "../features/reviews/schemas/firestore";
6
+ import type { OrderDocument } from "../features/orders/schemas/firestore";
6
7
  /** products — title, description, brand, taxonomy, condition, card/grading, specs. */
7
8
  export declare function buildProductSearchTxt(p: Partial<ProductDocument>): string[];
8
9
  /** stores — name, description, category. No payout details, no contact. */
@@ -21,3 +22,13 @@ export declare function buildEventSearchTxt(e: Partial<EventDocument>): string[]
21
22
  export declare function buildBlogSearchTxt(b: Partial<BlogPostDocument>): string[];
22
23
  /** reviews — title, comment, productTitle. `userName` is PII and excluded. */
23
24
  export declare function buildReviewSearchTxt(r: Partial<ReviewDocument>): string[];
25
+ /**
26
+ * orders — what an admin or seller actually looks an order up BY: the product,
27
+ * the store, the tracking number.
28
+ *
29
+ * 🛑 `userName`, `userEmail` and `sellerEmail` are ORDER_PII_FIELDS and are
30
+ * absent. `shippingAddress` is absent too — it is not in that registry, but it
31
+ * is an address, and a searchTxt for an address IS that address re-encoded.
32
+ * Indexing any of them would undo the encryption they are stored under (D1).
33
+ */
34
+ export declare function buildOrderSearchTxt(o: Partial<OrderDocument>): string[];
@@ -84,3 +84,20 @@ export function buildBlogSearchTxt(b) {
84
84
  export function buildReviewSearchTxt(r) {
85
85
  return buildSearchTxt([r.title, stripHtml(r.comment), r.productTitle]);
86
86
  }
87
+ /**
88
+ * orders — what an admin or seller actually looks an order up BY: the product,
89
+ * the store, the tracking number.
90
+ *
91
+ * 🛑 `userName`, `userEmail` and `sellerEmail` are ORDER_PII_FIELDS and are
92
+ * absent. `shippingAddress` is absent too — it is not in that registry, but it
93
+ * is an address, and a searchTxt for an address IS that address re-encoded.
94
+ * Indexing any of them would undo the encryption they are stored under (D1).
95
+ */
96
+ export function buildOrderSearchTxt(o) {
97
+ return buildSearchTxt([
98
+ o.productTitle,
99
+ o.storeName,
100
+ o.trackingNumber,
101
+ (o.items ?? []).map((i) => i.productTitle),
102
+ ]);
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.30.7",
3
+ "version": "4.30.8",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"