@fabriktor/fx 0.0.105 → 0.0.108

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.
@@ -26,7 +26,24 @@ type VendorInvoiceInsertOptions = Parameters<BillableOperator["insertOneVendorIn
26
26
  type VendorInvoiceReplaceOptions = Parameters<BillableOperator["replaceOneVendorInvoice"]>[0];
27
27
  type WarehouseInsertOptions = Parameters<BillableOperator["insertOneWarehouse"]>[0];
28
28
  type WarehouseReplaceOptions = Parameters<BillableOperator["replaceOneWarehouse"]>[0];
29
- export type SearchBillsOptions = OwnerSearchOptions & Partial<Pick<Bill, "is_estimate" | "is_canceled" | "type">>;
29
+ export declare const BillFilter: {
30
+ readonly All: "all";
31
+ readonly NotCancelled: "not_cancelled";
32
+ readonly Fixed: "fixed";
33
+ readonly Dynamic: "dynamic";
34
+ readonly Cancelled: "cancelled";
35
+ };
36
+ export type BillFilter = (typeof BillFilter)[keyof typeof BillFilter];
37
+ export declare function billFilterFields(value: BillFilter): Partial<Pick<Bill, "is_canceled" | "type">>;
38
+ type ItemSearchFields = Partial<Pick<Item, "folder_id" | "is_archived" | "quantifier_id">> & {
39
+ warehouse_id?: Item["per_warehouse_quantity"][number]["warehouse_id"];
40
+ };
41
+ type EquipmentSearchFields = Partial<Pick<Equipment, "folder_id" | "is_archived" | "warehouse_id" | "rental_type">>;
42
+ type ChargeSearchFields = Partial<Pick<Charge, "location_id" | "entity_id" | "warehouse_id" | "type" | "is_estimate" | "is_billed">>;
43
+ type MeterReadingSearchFields = Partial<Pick<MeterReading, "location_id" | "equipment_id">>;
44
+ export type SearchBillsOptions = OwnerSearchOptions & Partial<Pick<Bill, "is_estimate" | "is_canceled" | "type">> & {
45
+ filter?: BillFilter;
46
+ };
30
47
  export type SearchBillsPage = SearchPage<Bill>;
31
48
  export type CreateBillableFolderOptions = BillableFolderInsertOptions;
32
49
  export type ReplaceBillableFolderOptions = BillableFolderReplaceOptions;
@@ -34,31 +51,31 @@ export type SearchBillableFoldersOptions = OwnerSearchOptions;
34
51
  export type SearchBillableFoldersPage = SearchPage<BillableFolder>;
35
52
  export type CreateChargeOptions = ChargeInsertOptions;
36
53
  export type ReplaceChargeOptions = ChargeReplaceOptions;
37
- export type SearchChargesOptions = OwnerSearchOptions;
54
+ export type SearchChargesOptions = OwnerSearchOptions & ChargeSearchFields;
38
55
  export type SearchChargesPage = SearchPage<Charge>;
39
56
  export type CreateEquipmentOptions = EquipmentInsertOptions;
40
57
  export type ReplaceEquipmentOptions = EquipmentReplaceOptions;
41
- export type SearchEquipmentsOptions = OwnerSearchOptions;
58
+ export type SearchEquipmentsOptions = OwnerSearchOptions & EquipmentSearchFields;
42
59
  export type SearchEquipmentsPage = SearchPage<Equipment>;
43
60
  export type CreateItemOptions = ItemInsertOptions;
44
61
  export type ReplaceItemOptions = ItemReplaceOptions;
45
- export type SearchItemsOptions = OwnerSearchOptions;
62
+ export type SearchItemsOptions = OwnerSearchOptions & ItemSearchFields;
46
63
  export type SearchItemsPage = SearchPage<Item>;
47
64
  export type CreateLaborRateOptions = LaborRateInsertOptions;
48
65
  export type ReplaceLaborRateOptions = LaborRateReplaceOptions;
49
- export type SearchLaborRatesOptions = OwnerSearchOptions;
66
+ export type SearchLaborRatesOptions = OwnerSearchOptions & Partial<Pick<LaborRate, "is_archived">>;
50
67
  export type SearchLaborRatesPage = SearchPage<LaborRate>;
51
68
  export type CreateMeterReadingOptions = MeterReadingInsertOptions;
52
69
  export type ReplaceMeterReadingOptions = MeterReadingReplaceOptions;
53
- export type SearchMeterReadingsOptions = OwnerSearchOptions;
70
+ export type SearchMeterReadingsOptions = OwnerSearchOptions & MeterReadingSearchFields;
54
71
  export type SearchMeterReadingsPage = SearchPage<MeterReading>;
55
72
  export type CreateQuantifierOptions = QuantifierInsertOptions;
56
73
  export type ReplaceQuantifierOptions = QuantifierReplaceOptions;
57
- export type SearchQuantifiersOptions = OwnerSearchOptions;
74
+ export type SearchQuantifiersOptions = OwnerSearchOptions & Partial<Pick<Quantifier, "is_archived">>;
58
75
  export type SearchQuantifiersPage = SearchPage<Quantifier>;
59
76
  export type CreateTaxOptions = TaxInsertOptions;
60
77
  export type ReplaceTaxOptions = TaxReplaceOptions;
61
- export type SearchTaxesOptions = OwnerSearchOptions;
78
+ export type SearchTaxesOptions = OwnerSearchOptions & Partial<Pick<Tax, "is_archived">>;
62
79
  export type SearchTaxesPage = SearchPage<Tax>;
63
80
  export type SearchAllTaxesOptions = Omit<SearchTaxesOptions, "page">;
64
81
  export type CreateTmpBillNumberOptions = TmpBillNumberInsertOptions;
@@ -70,11 +87,11 @@ export type CreateVendorInvoiceOptions = VendorInvoiceInsertOptions & {
70
87
  allow_duplicate?: boolean;
71
88
  };
72
89
  export type ReplaceVendorInvoiceOptions = VendorInvoiceReplaceOptions;
73
- export type SearchVendorInvoicesOptions = OwnerSearchOptions;
90
+ export type SearchVendorInvoicesOptions = OwnerSearchOptions & Partial<Pick<VendorInvoice, "is_archived">>;
74
91
  export type SearchVendorInvoicesPage = SearchPage<VendorInvoice>;
75
92
  export type CreateWarehouseOptions = WarehouseInsertOptions;
76
93
  export type ReplaceWarehouseOptions = WarehouseReplaceOptions;
77
- export type SearchWarehousesOptions = OwnerSearchOptions;
94
+ export type SearchWarehousesOptions = OwnerSearchOptions & Partial<Pick<Warehouse, "is_archived">>;
78
95
  export type SearchWarehousesPage = SearchPage<Warehouse>;
79
96
  export type BillableFXOptions = {
80
97
  billable: BillableOperator;
@@ -111,14 +128,12 @@ export type BillableReadSearchOptions = SearchOptions & {
111
128
  employer_scope_ids?: Iterable<string>;
112
129
  query?: SearchQuery;
113
130
  };
114
- export type BillableItemReadSearchOptions = BillableReadSearchOptions & {
115
- warehouse_id?: string;
116
- quantifier_id?: string;
117
- };
118
- export type BillableEquipmentReadSearchOptions = BillableReadSearchOptions & {
119
- warehouse_id?: string;
120
- rental_type?: string;
121
- };
131
+ export type BillableItemReadSearchOptions = BillableReadSearchOptions & ItemSearchFields;
132
+ export type BillableEquipmentReadSearchOptions = BillableReadSearchOptions & EquipmentSearchFields;
133
+ export type BillableChargeReadSearchOptions = BillableReadSearchOptions & ChargeSearchFields;
134
+ export type BillableMeterReadingReadSearchOptions = BillableReadSearchOptions & MeterReadingSearchFields;
135
+ export type BillableQuantifierReadSearchOptions = BillableReadSearchOptions & Partial<Pick<Quantifier, "is_archived">>;
136
+ export type BillableWarehouseReadSearchOptions = BillableReadSearchOptions & Partial<Pick<Warehouse, "is_archived">>;
122
137
  export type GenerateFixedEstimateOptions = {
123
138
  in: GenerateBillPayload;
124
139
  };
@@ -150,7 +165,7 @@ export interface BillableFXOperator {
150
165
  createCharge(opts: CreateChargeOptions): Promise<Charge>;
151
166
  replaceCharge(opts: ReplaceChargeOptions): Promise<Charge>;
152
167
  searchCharges(opts: SearchChargesOptions): Promise<SearchChargesPage>;
153
- searchReadableCharges(opts: BillableReadSearchOptions): Promise<SearchChargesPage>;
168
+ searchReadableCharges(opts: BillableChargeReadSearchOptions): Promise<SearchChargesPage>;
154
169
  createEquipment(opts: CreateEquipmentOptions): Promise<Equipment>;
155
170
  replaceEquipment(opts: ReplaceEquipmentOptions): Promise<Equipment>;
156
171
  searchEquipments(opts: SearchEquipmentsOptions): Promise<SearchEquipmentsPage>;
@@ -165,11 +180,11 @@ export interface BillableFXOperator {
165
180
  createMeterReading(opts: CreateMeterReadingOptions): Promise<MeterReading>;
166
181
  replaceMeterReading(opts: ReplaceMeterReadingOptions): Promise<MeterReading>;
167
182
  searchMeterReadings(opts: SearchMeterReadingsOptions): Promise<SearchMeterReadingsPage>;
168
- searchReadableMeterReadings(opts: BillableReadSearchOptions): Promise<SearchMeterReadingsPage>;
183
+ searchReadableMeterReadings(opts: BillableMeterReadingReadSearchOptions): Promise<SearchMeterReadingsPage>;
169
184
  createQuantifier(opts: CreateQuantifierOptions): Promise<Quantifier>;
170
185
  replaceQuantifier(opts: ReplaceQuantifierOptions): Promise<Quantifier>;
171
186
  searchQuantifiers(opts: SearchQuantifiersOptions): Promise<SearchQuantifiersPage>;
172
- searchReadableQuantifiers(opts: BillableReadSearchOptions): Promise<SearchQuantifiersPage>;
187
+ searchReadableQuantifiers(opts: BillableQuantifierReadSearchOptions): Promise<SearchQuantifiersPage>;
173
188
  createTax(opts: CreateTaxOptions): Promise<Tax>;
174
189
  replaceTax(opts: ReplaceTaxOptions): Promise<Tax>;
175
190
  searchTaxes(opts: SearchTaxesOptions): Promise<SearchTaxesPage>;
@@ -183,7 +198,7 @@ export interface BillableFXOperator {
183
198
  createWarehouse(opts: CreateWarehouseOptions): Promise<Warehouse>;
184
199
  replaceWarehouse(opts: ReplaceWarehouseOptions): Promise<Warehouse>;
185
200
  searchWarehouses(opts: SearchWarehousesOptions): Promise<SearchWarehousesPage>;
186
- searchReadableWarehouses(opts: BillableReadSearchOptions): Promise<SearchWarehousesPage>;
201
+ searchReadableWarehouses(opts: BillableWarehouseReadSearchOptions): Promise<SearchWarehousesPage>;
187
202
  generateFixedEstimate(opts: GenerateFixedEstimateOptions): Promise<Bill>;
188
203
  acceptFixedEstimate(opts: AcceptFixedEstimateOptions): Promise<Bill>;
189
204
  generateDynamicEstimate(opts: GenerateDynamicEstimateOptions): Promise<Bill>;
@@ -205,7 +220,7 @@ export declare class BillableFX implements BillableFXOperator {
205
220
  createCharge(opts: CreateChargeOptions): Promise<Charge>;
206
221
  replaceCharge(opts: ReplaceChargeOptions): Promise<Charge>;
207
222
  searchCharges(opts: SearchChargesOptions): Promise<SearchChargesPage>;
208
- searchReadableCharges(opts: BillableReadSearchOptions): Promise<SearchChargesPage>;
223
+ searchReadableCharges(opts: BillableChargeReadSearchOptions): Promise<SearchChargesPage>;
209
224
  createEquipment(opts: CreateEquipmentOptions): Promise<Equipment>;
210
225
  replaceEquipment(opts: ReplaceEquipmentOptions): Promise<Equipment>;
211
226
  searchEquipments(opts: SearchEquipmentsOptions): Promise<SearchEquipmentsPage>;
@@ -220,11 +235,11 @@ export declare class BillableFX implements BillableFXOperator {
220
235
  createMeterReading(opts: CreateMeterReadingOptions): Promise<MeterReading>;
221
236
  replaceMeterReading(opts: ReplaceMeterReadingOptions): Promise<MeterReading>;
222
237
  searchMeterReadings(opts: SearchMeterReadingsOptions): Promise<SearchMeterReadingsPage>;
223
- searchReadableMeterReadings(opts: BillableReadSearchOptions): Promise<SearchMeterReadingsPage>;
238
+ searchReadableMeterReadings(opts: BillableMeterReadingReadSearchOptions): Promise<SearchMeterReadingsPage>;
224
239
  createQuantifier(opts: CreateQuantifierOptions): Promise<Quantifier>;
225
240
  replaceQuantifier(opts: ReplaceQuantifierOptions): Promise<Quantifier>;
226
241
  searchQuantifiers(opts: SearchQuantifiersOptions): Promise<SearchQuantifiersPage>;
227
- searchReadableQuantifiers(opts: BillableReadSearchOptions): Promise<SearchQuantifiersPage>;
242
+ searchReadableQuantifiers(opts: BillableQuantifierReadSearchOptions): Promise<SearchQuantifiersPage>;
228
243
  createTax(opts: CreateTaxOptions): Promise<Tax>;
229
244
  replaceTax(opts: ReplaceTaxOptions): Promise<Tax>;
230
245
  searchTaxes(opts: SearchTaxesOptions): Promise<SearchTaxesPage>;
@@ -238,7 +253,7 @@ export declare class BillableFX implements BillableFXOperator {
238
253
  createWarehouse(opts: CreateWarehouseOptions): Promise<Warehouse>;
239
254
  replaceWarehouse(opts: ReplaceWarehouseOptions): Promise<Warehouse>;
240
255
  searchWarehouses(opts: SearchWarehousesOptions): Promise<SearchWarehousesPage>;
241
- searchReadableWarehouses(opts: BillableReadSearchOptions): Promise<SearchWarehousesPage>;
256
+ searchReadableWarehouses(opts: BillableWarehouseReadSearchOptions): Promise<SearchWarehousesPage>;
242
257
  generateFixedEstimate(opts: GenerateFixedEstimateOptions): Promise<Bill>;
243
258
  acceptFixedEstimate(opts: AcceptFixedEstimateOptions): Promise<Bill>;
244
259
  generateDynamicEstimate(opts: GenerateDynamicEstimateOptions): Promise<Bill>;
@@ -249,7 +264,6 @@ export declare class BillableFX implements BillableFXOperator {
249
264
  vendorInvoiceFromBill(opts: VendorInvoiceFromBillOptions): Promise<VendorInvoice>;
250
265
  private generateBill;
251
266
  private checkVendorInvoiceNumber;
252
- private searchReadableActivity;
253
267
  private searchReadable;
254
268
  private findBill;
255
269
  private findBillableFolder;
@@ -3,13 +3,13 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- import { queryParams } from "@fabriktor/client";
6
+ import { mergeQueryOptions, queryNestedParam, queryParams, } from "@fabriktor/client";
7
7
  import { BillTypeDynamic, BillTypeFixed, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
8
8
  import { isZeroDateTime } from "../core/date.js";
9
9
  import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
10
10
  import { requiredOperationID, resolvedID } from "../core/id.js";
11
11
  import { runPreActionCheck } from "../core/pre-action.js";
12
- import { collectSearchResults, runOwnerScopeFilteredSearch, runOwnerScopeSearch, runOwnerSearch, withOwnerQuery, } from "../core/search.js";
12
+ import { collectSearchResults, runOwnerScopeSearch, runOwnerSearch, withOwnerQuery, } from "../core/search.js";
13
13
  const resourceBill = "bill";
14
14
  const msgCreatedBillIDMissing = "Created bill response did not contain an ID.";
15
15
  const msgBillMissing = "Bill could not be resolved after the mutation.";
@@ -56,6 +56,26 @@ const msgBillIDRequired = "Bill ID is required.";
56
56
  const msgBillIDMissing = "Bill response did not contain an ID.";
57
57
  const msgCompletableBillRequired = "Only an uncanceled invoice can be completed.";
58
58
  const msgAcceptedEstimateCancellation = "Cancel the accepted invoice instead of its estimate.";
59
+ // Composite sidebar states belong with their backend query mapping.
60
+ export const BillFilter = {
61
+ All: "all",
62
+ NotCancelled: "not_cancelled",
63
+ Fixed: BillTypeFixed,
64
+ Dynamic: BillTypeDynamic,
65
+ Cancelled: "cancelled",
66
+ };
67
+ export function billFilterFields(value) {
68
+ switch (value) {
69
+ case BillFilter.All:
70
+ return {};
71
+ case BillFilter.NotCancelled:
72
+ return { is_canceled: false };
73
+ case BillFilter.Cancelled:
74
+ return { is_canceled: true };
75
+ default:
76
+ return { is_canceled: false, type: value };
77
+ }
78
+ }
59
79
  export const BillableReadResource = {
60
80
  Bill: "bill",
61
81
  BillableFolder: "billable_folder",
@@ -114,6 +134,7 @@ export class BillableFX {
114
134
  is_estimate: opts.is_estimate,
115
135
  is_canceled: opts.is_canceled,
116
136
  type: opts.type,
137
+ ...(opts.filter === undefined ? {} : billFilterFields(opts.filter)),
117
138
  }),
118
139
  search: (searchOpts) => this.billable.searchManyBills(searchOpts),
119
140
  });
@@ -145,12 +166,20 @@ export class BillableFX {
145
166
  async searchCharges(opts) {
146
167
  return await runOwnerSearch({
147
168
  ...opts,
169
+ query: chargeSearchQuery(opts),
148
170
  search: (searchOpts) => this.billable.searchManyCharges(searchOpts),
149
171
  });
150
172
  }
151
173
  async searchReadableCharges(opts) {
152
- return await this.searchReadableActivity({
174
+ const employer_ids = activityEmployerScopeIDs(opts);
175
+ return await this.searchReadable({
153
176
  ...opts,
177
+ // owner_id identifies the creator; employer_id identifies their employer.
178
+ // An explicitly empty authorized employer set must not become All.
179
+ allowed_owner_ids: employer_ids?.size === 0 ? [] : activityOwnerIDs(opts.context),
180
+ query: mergeQueryOptions(opts.query, chargeSearchQuery(opts), employer_ids === undefined ? undefined : queryParams({
181
+ employer_id: [...employer_ids],
182
+ })),
154
183
  search: (searchOpts) => this.billable.searchManyCharges(searchOpts),
155
184
  });
156
185
  }
@@ -166,18 +195,15 @@ export class BillableFX {
166
195
  async searchEquipments(opts) {
167
196
  return await runOwnerSearch({
168
197
  ...opts,
198
+ query: equipmentSearchQuery(opts),
169
199
  search: (searchOpts) => this.billable.searchManyEquipments(searchOpts),
170
200
  });
171
201
  }
172
202
  async searchReadableEquipments(opts) {
173
- const { warehouse_id, rental_type, ...search_opts } = opts;
174
203
  return await this.searchReadable({
175
- ...search_opts,
204
+ ...opts,
205
+ query: mergeQueryOptions(opts.query, equipmentSearchQuery(opts)),
176
206
  allowed_owner_ids: catalogOwnerIDs(opts.context, opts.employer_scope_ids),
177
- filter: warehouse_id === undefined && rental_type === undefined
178
- ? undefined
179
- : (equipment) => (warehouse_id === undefined || equipment.warehouse_id === warehouse_id) &&
180
- (rental_type === undefined || equipment.rental_type === rental_type),
181
207
  search: (searchOpts) => this.billable.searchManyEquipments(searchOpts),
182
208
  });
183
209
  }
@@ -193,19 +219,15 @@ export class BillableFX {
193
219
  async searchItems(opts) {
194
220
  return await runOwnerSearch({
195
221
  ...opts,
222
+ query: itemSearchQuery(opts),
196
223
  search: (searchOpts) => this.billable.searchManyItems(searchOpts),
197
224
  });
198
225
  }
199
226
  async searchReadableItems(opts) {
200
- const { warehouse_id, quantifier_id, ...search_opts } = opts;
201
227
  return await this.searchReadable({
202
- ...search_opts,
228
+ ...opts,
229
+ query: mergeQueryOptions(opts.query, itemSearchQuery(opts)),
203
230
  allowed_owner_ids: catalogOwnerIDs(opts.context, opts.employer_scope_ids),
204
- filter: warehouse_id === undefined && quantifier_id === undefined
205
- ? undefined
206
- : (item) => (warehouse_id === undefined ||
207
- item.per_warehouse_quantity.some((quantity) => quantity.warehouse_id === warehouse_id)) &&
208
- (quantifier_id === undefined || item.quantifier_id === quantifier_id),
209
231
  search: (searchOpts) => this.billable.searchManyItems(searchOpts),
210
232
  });
211
233
  }
@@ -221,6 +243,7 @@ export class BillableFX {
221
243
  async searchLaborRates(opts) {
222
244
  return await runOwnerSearch({
223
245
  ...opts,
246
+ query: queryParams({ is_archived: opts.is_archived }),
224
247
  search: (searchOpts) => this.billable.searchManyLaborRates(searchOpts),
225
248
  });
226
249
  }
@@ -236,12 +259,19 @@ export class BillableFX {
236
259
  async searchMeterReadings(opts) {
237
260
  return await runOwnerSearch({
238
261
  ...opts,
262
+ query: meterReadingSearchQuery(opts),
239
263
  search: (searchOpts) => this.billable.searchManyMeterReadings(searchOpts),
240
264
  });
241
265
  }
242
266
  async searchReadableMeterReadings(opts) {
243
- return await this.searchReadableActivity({
267
+ const employer_ids = activityEmployerScopeIDs(opts);
268
+ return await this.searchReadable({
244
269
  ...opts,
270
+ // An explicitly empty authorized scope must stay empty, not become All.
271
+ allowed_owner_ids: employer_ids?.size === 0 ? [] : activityOwnerIDs(opts.context),
272
+ query: mergeQueryOptions(opts.query, meterReadingSearchQuery(opts), employer_ids === undefined ? undefined : queryParams({
273
+ employer_id: [...employer_ids],
274
+ })),
245
275
  search: (searchOpts) => this.billable.searchManyMeterReadings(searchOpts),
246
276
  });
247
277
  }
@@ -257,12 +287,16 @@ export class BillableFX {
257
287
  async searchQuantifiers(opts) {
258
288
  return await runOwnerSearch({
259
289
  ...opts,
290
+ query: queryParams({ is_archived: opts.is_archived }),
260
291
  search: (searchOpts) => this.billable.searchManyQuantifiers(searchOpts),
261
292
  });
262
293
  }
263
294
  async searchReadableQuantifiers(opts) {
264
295
  return await this.searchReadable({
265
296
  ...opts,
297
+ query: mergeQueryOptions(opts.query, queryParams({
298
+ ...(opts.is_archived === undefined ? {} : { is_archived: opts.is_archived }),
299
+ })),
266
300
  allowed_owner_ids: catalogOwnerIDs(opts.context, opts.employer_scope_ids),
267
301
  search: (searchOpts) => this.billable.searchManyQuantifiers(searchOpts),
268
302
  });
@@ -279,6 +313,7 @@ export class BillableFX {
279
313
  async searchTaxes(opts) {
280
314
  return await runOwnerSearch({
281
315
  ...opts,
316
+ query: queryParams({ is_archived: opts.is_archived }),
282
317
  search: (searchOpts) => this.billable.searchManyTaxes(searchOpts),
283
318
  });
284
319
  }
@@ -329,6 +364,7 @@ export class BillableFX {
329
364
  async searchVendorInvoices(opts) {
330
365
  return await runOwnerSearch({
331
366
  ...opts,
367
+ query: queryParams({ is_archived: opts.is_archived }),
332
368
  search: (searchOpts) => this.billable.searchManyVendorInvoices(searchOpts),
333
369
  });
334
370
  }
@@ -344,12 +380,16 @@ export class BillableFX {
344
380
  async searchWarehouses(opts) {
345
381
  return await runOwnerSearch({
346
382
  ...opts,
383
+ query: queryParams({ is_archived: opts.is_archived }),
347
384
  search: (searchOpts) => this.billable.searchManyWarehouses(searchOpts),
348
385
  });
349
386
  }
350
387
  async searchReadableWarehouses(opts) {
351
388
  return await this.searchReadable({
352
389
  ...opts,
390
+ query: mergeQueryOptions(opts.query, queryParams({
391
+ ...(opts.is_archived === undefined ? {} : { is_archived: opts.is_archived }),
392
+ })),
353
393
  allowed_owner_ids: catalogOwnerIDs(opts.context, opts.employer_scope_ids),
354
394
  search: (searchOpts) => this.billable.searchManyWarehouses(searchOpts),
355
395
  });
@@ -424,38 +464,12 @@ export class BillableFX {
424
464
  },
425
465
  });
426
466
  }
427
- async searchReadableActivity(opts) {
428
- const employer_scope_ids = activityEmployerScopeIDs(opts);
429
- return await this.searchReadable({
430
- ...opts,
431
- allowed_owner_ids: activityOwnerIDs(opts.context),
432
- filter: employer_scope_ids === undefined
433
- ? undefined
434
- : (value) => employer_scope_ids.has(value.employer_id),
435
- });
436
- }
437
467
  async searchReadable(opts) {
438
- const { context: _context, owner_ids: requested_owner_ids, employer_scope_ids: _employer_scope_ids, allowed_owner_ids, search, filter, query, ...searchOpts } = opts;
468
+ const { context: _context, owner_ids: requested_owner_ids, employer_scope_ids: _employer_scope_ids, allowed_owner_ids, search, query, ...searchOpts } = opts;
439
469
  const owner_ids = restrictOwnerIDs(allowed_owner_ids, requested_owner_ids);
440
- const page = filter === undefined
441
- ? await runOwnerScopeSearch({
442
- ...searchOpts,
443
- owner_ids,
444
- query,
445
- search,
446
- })
447
- : await runOwnerScopeFilteredSearch({
448
- ...searchOpts,
449
- owner_ids,
450
- query,
451
- search,
452
- filter,
453
- compare: compareCreatedAtDescending,
454
- });
455
- return {
456
- ...page,
457
- results: [...page.results].sort(compareCreatedAtDescending),
458
- };
470
+ // Keep each backend page in its returned order. The owner contract requires
471
+ // separate requests; do not manufacture a cross-owner sort in the frontend.
472
+ return await runOwnerScopeSearch({ ...searchOpts, owner_ids, query, search });
459
473
  }
460
474
  async findBill(id) {
461
475
  const out = await this.billable.findOneBill({
@@ -712,6 +726,55 @@ function catalogOwnerIDs(context, employer_scope_ids) {
712
726
  }
713
727
  return uniqueIDs([context.owner_id]);
714
728
  }
729
+ function itemSearchQuery(opts) {
730
+ return mergeQueryOptions(queryParams({
731
+ ...(opts.folder_id === undefined
732
+ ? {}
733
+ : { folder_id: resolvedID(opts.folder_id) }),
734
+ ...(opts.is_archived === undefined ? {} : { is_archived: opts.is_archived }),
735
+ ...(opts.quantifier_id === undefined
736
+ ? {}
737
+ : { quantifier_id: resolvedID(opts.quantifier_id) }),
738
+ }), opts.warehouse_id === undefined ? undefined : queryNestedParam(["per_warehouse_quantity", "warehouse_id"], resolvedID(opts.warehouse_id)));
739
+ }
740
+ function equipmentSearchQuery(opts) {
741
+ return queryParams({
742
+ ...(opts.folder_id === undefined
743
+ ? {}
744
+ : { folder_id: resolvedID(opts.folder_id) }),
745
+ ...(opts.is_archived === undefined ? {} : { is_archived: opts.is_archived }),
746
+ ...(opts.warehouse_id === undefined
747
+ ? {}
748
+ : { warehouse_id: resolvedID(opts.warehouse_id) }),
749
+ ...(opts.rental_type === undefined ? {} : { rental_type: opts.rental_type }),
750
+ });
751
+ }
752
+ function chargeSearchQuery(opts) {
753
+ return queryParams({
754
+ ...(opts.location_id === undefined
755
+ ? {}
756
+ : { location_id: resolvedID(opts.location_id) }),
757
+ ...(opts.entity_id === undefined
758
+ ? {}
759
+ : { entity_id: resolvedID(opts.entity_id) }),
760
+ ...(opts.warehouse_id === undefined
761
+ ? {}
762
+ : { warehouse_id: resolvedID(opts.warehouse_id) }),
763
+ ...(opts.type === undefined ? {} : { type: opts.type }),
764
+ ...(opts.is_estimate === undefined ? {} : { is_estimate: opts.is_estimate }),
765
+ ...(opts.is_billed === undefined ? {} : { is_billed: opts.is_billed }),
766
+ });
767
+ }
768
+ function meterReadingSearchQuery(opts) {
769
+ return queryParams({
770
+ ...(opts.location_id === undefined
771
+ ? {}
772
+ : { location_id: resolvedID(opts.location_id) }),
773
+ ...(opts.equipment_id === undefined
774
+ ? {}
775
+ : { equipment_id: resolvedID(opts.equipment_id) }),
776
+ });
777
+ }
715
778
  function activityEmployerScopeIDs(opts) {
716
779
  if (opts.context.active_mode === UserTypeEmployer) {
717
780
  return new Set([opts.context.owner_id]);
@@ -739,13 +802,6 @@ function restrictOwnerIDs(allowed, requested) {
739
802
  function uniqueIDs(ids) {
740
803
  return [...new Set([...ids].map((id) => id.trim()).filter((id) => id !== ""))];
741
804
  }
742
- function compareCreatedAtDescending(left, right) {
743
- return timestamp(right.created_at) - timestamp(left.created_at);
744
- }
745
- function timestamp(value) {
746
- const out = globalThis.Date.parse(value);
747
- return Number.isFinite(out) ? out : 0;
748
- }
749
805
  export function newBillableFX(opts) {
750
806
  return new BillableFX(opts);
751
807
  }
@@ -0,0 +1,30 @@
1
+ import { type SearchOptions, type SearchPage, type SearchResultWithID } from "./search.js";
2
+ export type SearchState<T> = {
3
+ readonly results: readonly T[];
4
+ readonly ready: boolean;
5
+ readonly loading: boolean;
6
+ readonly loading_more: boolean;
7
+ readonly error: unknown;
8
+ readonly has_more: boolean;
9
+ readonly total_count?: number;
10
+ };
11
+ export type SearchControllerOptions<T, Options extends SearchOptions> = {
12
+ search: (opts: Options) => Promise<SearchPage<T>>;
13
+ on_change: (state: SearchState<T>) => void;
14
+ };
15
+ export declare class SearchController<T extends SearchResultWithID, Options extends SearchOptions> {
16
+ private readonly options;
17
+ private selection?;
18
+ private request?;
19
+ private next_page;
20
+ private current;
21
+ constructor(options: SearchControllerOptions<T, Options>);
22
+ get state(): SearchState<T>;
23
+ clear(): void;
24
+ replace(selection: Options): Promise<boolean>;
25
+ loadMore(): Promise<boolean>;
26
+ retry(): Promise<boolean>;
27
+ private load;
28
+ private emptyState;
29
+ private publish;
30
+ }
@@ -0,0 +1,111 @@
1
+ // Copyright (C) Fabriktor, Inc. 2025-present.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ // not use this file except in compliance with the License. You may obtain
5
+ // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { appendUniqueSearchResults, normalizeSearchPage, } from "./search.js";
7
+ import { errUnknown } from "./err.js";
8
+ // Holds only the current selection and its displayed pages. Previous queries
9
+ // are discarded, and revisiting a selection always makes a backend request.
10
+ export class SearchController {
11
+ options;
12
+ selection;
13
+ request;
14
+ next_page = normalizeSearchPage();
15
+ current = this.emptyState();
16
+ constructor(options) {
17
+ this.options = options;
18
+ }
19
+ get state() {
20
+ return this.current;
21
+ }
22
+ // Invalidate immediately, including during text-search debounce or teardown.
23
+ clear() {
24
+ this.request?.abort();
25
+ this.request = undefined;
26
+ this.selection = undefined;
27
+ this.next_page = normalizeSearchPage();
28
+ this.publish(this.emptyState());
29
+ }
30
+ async replace(selection) {
31
+ this.clear();
32
+ this.selection = { ...selection };
33
+ return await this.load(false);
34
+ }
35
+ async loadMore() {
36
+ if (!this.current.has_more || this.current.error !== undefined) {
37
+ return false;
38
+ }
39
+ return await this.load(true);
40
+ }
41
+ async retry() {
42
+ return await this.load(this.next_page > normalizeSearchPage());
43
+ }
44
+ async load(append) {
45
+ if (!this.selection || this.current.loading || this.current.loading_more) {
46
+ return false;
47
+ }
48
+ const request = new AbortController();
49
+ this.request = request;
50
+ this.publish({
51
+ ...this.current,
52
+ loading: !append,
53
+ loading_more: append,
54
+ error: undefined,
55
+ });
56
+ try {
57
+ const page = await this.options.search({
58
+ ...this.selection,
59
+ page: this.next_page,
60
+ signal: request.signal,
61
+ });
62
+ if (this.request !== request || request.signal.aborted) {
63
+ return false;
64
+ }
65
+ this.next_page = page.next_page;
66
+ this.publish({
67
+ results: append
68
+ ? appendUniqueSearchResults(this.current.results, page.results)
69
+ : page.results,
70
+ ready: true,
71
+ loading: false,
72
+ loading_more: false,
73
+ error: undefined,
74
+ has_more: page.has_more,
75
+ total_count: page.total_count,
76
+ });
77
+ return true;
78
+ }
79
+ catch (error) {
80
+ if (this.request !== request || request.signal.aborted) {
81
+ return false;
82
+ }
83
+ this.publish({
84
+ ...this.current,
85
+ loading: false,
86
+ loading_more: false,
87
+ error: errUnknown(error),
88
+ });
89
+ return false;
90
+ }
91
+ finally {
92
+ if (this.request === request) {
93
+ this.request = undefined;
94
+ }
95
+ }
96
+ }
97
+ emptyState() {
98
+ return {
99
+ results: [],
100
+ ready: false,
101
+ loading: false,
102
+ loading_more: false,
103
+ error: undefined,
104
+ has_more: false,
105
+ };
106
+ }
107
+ publish(state) {
108
+ this.current = state;
109
+ this.options.on_change(state);
110
+ }
111
+ }
@@ -4,7 +4,7 @@ export type SearchPaginationOptions = {
4
4
  page?: number;
5
5
  per_page?: number;
6
6
  };
7
- export type SearchOptions = SearchPaginationOptions & {
7
+ export type SearchOptions = SearchPaginationOptions & Pick<SearchManyOptionsCRUD, "signal"> & {
8
8
  q?: string;
9
9
  };
10
10
  export type RequiredSearchOptions = SearchPaginationOptions & {
@@ -54,3 +54,7 @@ export declare function runSearch<T>(opts: RunSearchOptions<T>): Promise<SearchP
54
54
  export declare function normalizeSearchPage(value?: number): number;
55
55
  export declare function normalizeSearchPerPage(value?: number): number;
56
56
  export declare function normalizeSearchQuery(value?: string): string | undefined;
57
+ export type SearchResultWithID = {
58
+ readonly id: string;
59
+ };
60
+ export declare function appendUniqueSearchResults<T extends SearchResultWithID>(current: readonly T[], next: readonly T[]): T[];
@@ -48,6 +48,7 @@ export async function runOwnerScopeSearch(opts) {
48
48
  per_page,
49
49
  query: opts.query,
50
50
  search: opts.search,
51
+ signal: opts.signal,
51
52
  })));
52
53
  const results = new Map();
53
54
  for (const ownerPage of pages) {
@@ -94,6 +95,7 @@ export async function runOwnerScopeFilteredSearch(opts) {
94
95
  per_page,
95
96
  query: opts.query,
96
97
  search: opts.search,
98
+ signal: opts.signal,
97
99
  }),
98
100
  })));
99
101
  for (const owner_page of pages) {
@@ -147,6 +149,7 @@ export async function runSearch(opts) {
147
149
  };
148
150
  const raw = await opts.search({
149
151
  query,
152
+ signal: opts.signal,
150
153
  });
151
154
  return resolveSearchPage({
152
155
  raw,
@@ -236,3 +239,17 @@ function maxDefined(values) {
236
239
  }
237
240
  return Math.max(...values.map((value) => value ?? 0));
238
241
  }
242
+ export function appendUniqueSearchResults(current, next) {
243
+ const out = [...current];
244
+ const indexes = new Map(out.map((item, index) => [item.id, index]));
245
+ for (const item of next) {
246
+ const index = indexes.get(item.id);
247
+ if (index === undefined) {
248
+ indexes.set(item.id, out.length);
249
+ out.push(item);
250
+ continue;
251
+ }
252
+ out[index] = item;
253
+ }
254
+ return out;
255
+ }
@@ -12,6 +12,7 @@ export * from "./core/pre-action.js";
12
12
  export * from "./core/pull.js";
13
13
  export * from "./core/queue-store.js";
14
14
  export * from "./core/queue.js";
15
+ export * from "./core/search-controller.js";
15
16
  export * from "./core/search.js";
16
17
  export * from "./feed/feed.js";
17
18
  export * from "./fulfillments/fulfillments.js";
package/dist/src/index.js CHANGED
@@ -12,6 +12,7 @@ export * from "./core/pre-action.js";
12
12
  export * from "./core/pull.js";
13
13
  export * from "./core/queue-store.js";
14
14
  export * from "./core/queue.js";
15
+ export * from "./core/search-controller.js";
15
16
  export * from "./core/search.js";
16
17
  export * from "./feed/feed.js";
17
18
  export * from "./fulfillments/fulfillments.js";
@@ -1,5 +1,5 @@
1
- import type { MemosOperator } from "@fabriktor/client";
2
- import type { Memo, MemoFolder } from "@fabriktor/schema";
1
+ import { type MemosOperator } from "@fabriktor/client";
2
+ import { type Memo, type MemoFolder } from "@fabriktor/schema";
3
3
  import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
4
  type MemoInsertOptions = Parameters<MemosOperator["insertOneMemo"]>[0];
5
5
  type MemoReplaceOptions = Parameters<MemosOperator["replaceOneMemo"]>[0];
@@ -9,7 +9,7 @@ export type CreateMemoOptions = MemoInsertOptions;
9
9
  export type ReplaceMemoOptions = MemoReplaceOptions;
10
10
  export type CreateMemoFolderOptions = MemoFolderInsertOptions;
11
11
  export type ReplaceMemoFolderOptions = MemoFolderReplaceOptions;
12
- export type SearchMemosOptions = OwnerSearchOptions;
12
+ export type SearchMemosOptions = OwnerSearchOptions & Partial<Pick<Memo, "is_archived" | "folder_id">>;
13
13
  export type SearchMemoFoldersOptions = OwnerSearchOptions;
14
14
  export type SearchMemosPage = SearchPage<Memo>;
15
15
  export type SearchMemoFoldersPage = SearchPage<MemoFolder>;
@@ -3,13 +3,16 @@
3
3
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- import { errResolveFailed } from "../core/err.js";
7
- import { requiredOperationID } from "../core/id.js";
6
+ import { queryParams } from "@fabriktor/client";
7
+ import { OwnerIDKey } from "@fabriktor/schema";
8
+ import { errRequired, errResolveFailed } from "../core/err.js";
9
+ import { requiredOperationID, resolvedID } from "../core/id.js";
8
10
  import { runOwnerSearch, } from "../core/search.js";
9
11
  const resourceMemo = "memo";
10
12
  const resourceMemoFolder = "memo_folder";
11
13
  const msgCreatedMemoIDMissing = "Created memo response did not contain an ID.";
12
14
  const msgMemoMissing = "Memo could not be resolved after the mutation.";
15
+ const msgMemoOwnerRequired = "Memo owner is required.";
13
16
  const msgCreatedMemoFolderIDMissing = "Created memo folder response did not contain an ID.";
14
17
  const msgMemoFolderMissing = "Memo folder could not be resolved after the mutation.";
15
18
  export class MemosFX {
@@ -36,8 +39,18 @@ export class MemosFX {
36
39
  return await this.findMemoFolder(opts.id);
37
40
  }
38
41
  async searchMemos(opts) {
42
+ const { is_archived, folder_id, ...searchOpts } = opts;
43
+ const owner_id = resolvedID(opts.owner_id);
44
+ if (owner_id === undefined) {
45
+ throw errRequired({ field: OwnerIDKey, msg: msgMemoOwnerRequired });
46
+ }
39
47
  return await runOwnerSearch({
40
- ...opts,
48
+ ...searchOpts,
49
+ owner_id,
50
+ query: queryParams({
51
+ is_archived,
52
+ folder_id: resolvedID(folder_id),
53
+ }),
41
54
  search: (searchOpts) => this.memos.searchManyMemos(searchOpts),
42
55
  });
43
56
  }
@@ -1,5 +1,5 @@
1
1
  import { type CreditLedgerEntry, type Payment, type PmtAccount, type PmtBankAccount, type PmtCard, type PmtCustomer, type PmtPublicSession, type PmtRefund } from "@fabriktor/schema";
2
- import type { PaymentsOperator } from "@fabriktor/client";
2
+ import { type PaymentsOperator } from "@fabriktor/client";
3
3
  import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
4
  type PurchaseCreditsPayload = Parameters<PaymentsOperator["purchaseCredits"]>[0];
5
5
  type OpenPublicSessionPayload = Parameters<PaymentsOperator["openPmtPublicSession"]>[0];
@@ -33,7 +33,7 @@ export type SearchPmtAccountsOptions = OwnerSearchOptions;
33
33
  export type SearchPmtAccountsPage = SearchPage<PmtAccount>;
34
34
  export type CreatePmtBankAccountOptions = PmtBankAccountInsertOptions;
35
35
  export type ReplacePmtBankAccountOptions = PmtBankAccountReplaceOptions;
36
- export type SearchPmtBankAccountsOptions = OwnerSearchOptions;
36
+ export type SearchPmtBankAccountsOptions = OwnerSearchOptions & Partial<Pick<PmtBankAccount, "psp_account_psp_id">>;
37
37
  export type SearchPmtBankAccountsPage = SearchPage<PmtBankAccount>;
38
38
  export type CreatePmtCardOptions = PmtCardInsertOptions;
39
39
  export type ReplacePmtCardOptions = PmtCardReplaceOptions;
@@ -69,6 +69,7 @@ export type PmtAccountAccess = {
69
69
  };
70
70
  export type GetPmtAccountAccessOptions = {
71
71
  owner_id: string;
72
+ signal?: AbortSignal;
72
73
  };
73
74
  export declare function isPmtAccountReady(account: PmtAccount | null): boolean;
74
75
  export declare function currentPmtAccount(accounts: readonly PmtAccount[]): PmtAccount | null;
@@ -4,6 +4,7 @@
4
4
  // not use this file except in compliance with the License. You may obtain
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, } from "@fabriktor/schema";
7
+ import { queryParams } from "@fabriktor/client";
7
8
  import { errResolveFailed, errValidation } from "../core/err.js";
8
9
  import { requiredOperationID, resolvedID } from "../core/id.js";
9
10
  import { runOwnerSearch, } from "../core/search.js";
@@ -154,9 +155,11 @@ export class PaymentsFX {
154
155
  const accounts = [];
155
156
  let page = 1;
156
157
  while (true) {
158
+ opts.signal?.throwIfAborted();
157
159
  const out = await this.searchPmtAccounts({
158
160
  owner_id: opts.owner_id,
159
161
  page,
162
+ signal: opts.signal,
160
163
  });
161
164
  accounts.push(...out.results);
162
165
  const account = currentPmtAccount(accounts);
@@ -178,6 +181,9 @@ export class PaymentsFX {
178
181
  async searchPmtBankAccounts(opts) {
179
182
  return await runOwnerSearch({
180
183
  ...opts,
184
+ query: queryParams({
185
+ psp_account_psp_id: resolvedID(opts.psp_account_psp_id),
186
+ }),
181
187
  search: (searchOpts) => this.payments.searchManyPmtBankAccounts(searchOpts),
182
188
  });
183
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.105",
3
+ "version": "0.0.108",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.82",
20
+ "@fabriktor/client": "0.0.83",
21
21
  "@fabriktor/fx": "0.0.94",
22
22
  "@fabriktor/schema": "0.0.63"
23
23
  }