@fabriktor/fx 0.0.104 → 0.0.106
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/src/billable/billable.d.ts +41 -27
- package/dist/src/billable/billable.js +110 -54
- package/dist/src/chat/send-queue.d.ts +8 -1
- package/dist/src/chat/send-queue.js +108 -40
- package/dist/src/core/date.d.ts +4 -0
- package/dist/src/core/date.js +46 -1
- package/dist/src/core/queue.d.ts +10 -0
- package/dist/src/core/queue.js +33 -0
- package/dist/src/core/search-controller.d.ts +30 -0
- package/dist/src/core/search-controller.js +111 -0
- package/dist/src/core/search.d.ts +5 -1
- package/dist/src/core/search.js +17 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/memos/memos.d.ts +3 -3
- package/dist/src/memos/memos.js +16 -3
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
export type
|
|
119
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatOperator } from "@fabriktor/client";
|
|
2
2
|
import type { Chat, InChat } from "@fabriktor/schema";
|
|
3
|
-
import { type QueueStore, type QueueLock } from "../core/queue.js";
|
|
3
|
+
import { type QueueRetry, type QueueStore, type QueueLock } from "../core/queue.js";
|
|
4
4
|
export declare const ChatSendStatus: {
|
|
5
5
|
readonly Saving: "saving";
|
|
6
6
|
readonly Queued: "queued";
|
|
@@ -28,6 +28,7 @@ export type QueuedChat = {
|
|
|
28
28
|
chat?: Chat;
|
|
29
29
|
attachments: ChatSendAttachments;
|
|
30
30
|
attempted: boolean;
|
|
31
|
+
retry?: QueueRetry;
|
|
31
32
|
upload_key: string;
|
|
32
33
|
personal_box_key: string;
|
|
33
34
|
};
|
|
@@ -64,6 +65,9 @@ export declare class ChatSendQueue {
|
|
|
64
65
|
private error?;
|
|
65
66
|
private enabled;
|
|
66
67
|
private revision;
|
|
68
|
+
private readonly wakeup;
|
|
69
|
+
private storageRetry?;
|
|
70
|
+
private pendingCreates;
|
|
67
71
|
constructor(opts: ChatSendQueueOptions);
|
|
68
72
|
get hasUnsavedMessages(): boolean;
|
|
69
73
|
get snapshot(): ChatSendSnapshot;
|
|
@@ -80,6 +84,9 @@ export declare class ChatSendQueue {
|
|
|
80
84
|
private startConfirmation;
|
|
81
85
|
private startUpload;
|
|
82
86
|
private recoverUpload;
|
|
87
|
+
private waitForRetry;
|
|
88
|
+
private recoverStorage;
|
|
89
|
+
private scheduleRecovery;
|
|
83
90
|
private uploadLock;
|
|
84
91
|
private confirmationLock;
|
|
85
92
|
private roomLock;
|
|
@@ -4,7 +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 { requiredOperationID } from "../core/id.js";
|
|
7
|
-
import { PersistentQueue, } from "../core/queue.js";
|
|
7
|
+
import { PersistentQueue, QueueWakeup, nextQueueRetry, } from "../core/queue.js";
|
|
8
8
|
import { newOptimisticChat } from "./optimistic.js";
|
|
9
9
|
export const ChatSendStatus = {
|
|
10
10
|
Saving: "saving",
|
|
@@ -17,7 +17,8 @@ export const ChatSendStatus = {
|
|
|
17
17
|
UploadInterrupted: "upload_interrupted",
|
|
18
18
|
};
|
|
19
19
|
// Persistent creation FIFO per account and room. Creation never advances beyond
|
|
20
|
-
//
|
|
20
|
+
// an uncertain head: it recovers automatically with its original request key.
|
|
21
|
+
// Retrieval, attachments, other rooms and other users are independent.
|
|
21
22
|
export class ChatSendQueue {
|
|
22
23
|
opts;
|
|
23
24
|
owner = "";
|
|
@@ -36,6 +37,9 @@ export class ChatSendQueue {
|
|
|
36
37
|
error;
|
|
37
38
|
enabled = false;
|
|
38
39
|
revision = 0;
|
|
40
|
+
wakeup = new QueueWakeup();
|
|
41
|
+
storageRetry;
|
|
42
|
+
pendingCreates = new Map();
|
|
39
43
|
constructor(opts) {
|
|
40
44
|
this.opts = opts;
|
|
41
45
|
this.queue = new PersistentQueue(opts.store, opts.lock);
|
|
@@ -58,7 +62,8 @@ export class ChatSendQueue {
|
|
|
58
62
|
}
|
|
59
63
|
async start(ownerID) {
|
|
60
64
|
if (this.enabled && this.owner === ownerID) {
|
|
61
|
-
|
|
65
|
+
this.persist();
|
|
66
|
+
await this.writes;
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
this.pause();
|
|
@@ -72,11 +77,13 @@ export class ChatSendQueue {
|
|
|
72
77
|
this.enabled = ownerID !== "";
|
|
73
78
|
this.controller = new AbortController();
|
|
74
79
|
this.publish();
|
|
75
|
-
|
|
80
|
+
this.persist();
|
|
81
|
+
await this.writes;
|
|
76
82
|
}
|
|
77
83
|
pause() {
|
|
78
84
|
this.enabled = false;
|
|
79
85
|
this.controller.abort();
|
|
86
|
+
this.wakeup.cancel();
|
|
80
87
|
}
|
|
81
88
|
enqueue(input, attachments) {
|
|
82
89
|
if (!this.enabled || !this.owner) {
|
|
@@ -106,23 +113,28 @@ export class ChatSendQueue {
|
|
|
106
113
|
this.writes = this.writes.then(async () => {
|
|
107
114
|
const blocked = new Set();
|
|
108
115
|
for (const entry of this.pendingSaves.values()) {
|
|
109
|
-
if (entry.
|
|
116
|
+
if (!this.current(entry.owner_id, this.controller.signal))
|
|
117
|
+
continue;
|
|
118
|
+
if (this.waitForRetry(entry) || blocked.has(entry.owner_id)) {
|
|
110
119
|
blocked.add(entry.owner_id);
|
|
111
|
-
}
|
|
112
|
-
if (blocked.has(entry.owner_id)) {
|
|
113
120
|
continue;
|
|
114
121
|
}
|
|
115
122
|
try {
|
|
116
123
|
const saved = await this.queue.enqueue({
|
|
117
124
|
...entry,
|
|
118
125
|
status: ChatSendStatus.Queued,
|
|
126
|
+
retry: undefined,
|
|
119
127
|
});
|
|
120
128
|
this.pendingSaves.delete(entry.id);
|
|
121
129
|
this.replace(saved);
|
|
122
130
|
this.opts.changed?.();
|
|
123
131
|
}
|
|
124
132
|
catch (err) {
|
|
125
|
-
const failed = {
|
|
133
|
+
const failed = {
|
|
134
|
+
...entry,
|
|
135
|
+
status: ChatSendStatus.Failed,
|
|
136
|
+
retry: nextQueueRetry(entry.retry),
|
|
137
|
+
};
|
|
126
138
|
this.pendingSaves.set(entry.id, failed);
|
|
127
139
|
this.replace(failed);
|
|
128
140
|
if (entry.owner_id === this.owner) {
|
|
@@ -130,6 +142,7 @@ export class ChatSendQueue {
|
|
|
130
142
|
this.publish();
|
|
131
143
|
}
|
|
132
144
|
blocked.add(entry.owner_id);
|
|
145
|
+
this.waitForRetry(failed);
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
await this.refresh();
|
|
@@ -166,7 +179,10 @@ export class ChatSendQueue {
|
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
181
|
this.error = undefined;
|
|
182
|
+
this.storageRetry = undefined;
|
|
169
183
|
this.publish();
|
|
184
|
+
if (stored.length === 0 && this.pendingSaves.size === 0)
|
|
185
|
+
this.wakeup.cancel();
|
|
170
186
|
for (const room of new Set(stored
|
|
171
187
|
.filter((entry) => !entry.chat && !entry.server_id)
|
|
172
188
|
.map((entry) => entry.input.chat_room_id))) {
|
|
@@ -185,6 +201,7 @@ export class ChatSendQueue {
|
|
|
185
201
|
if (this.current(owner, signal)) {
|
|
186
202
|
this.error = err;
|
|
187
203
|
this.publish();
|
|
204
|
+
this.recoverStorage();
|
|
188
205
|
}
|
|
189
206
|
}
|
|
190
207
|
}
|
|
@@ -197,7 +214,11 @@ export class ChatSendQueue {
|
|
|
197
214
|
if (entry.status !== ChatSendStatus.Failed) {
|
|
198
215
|
return;
|
|
199
216
|
}
|
|
200
|
-
const saving = {
|
|
217
|
+
const saving = {
|
|
218
|
+
...entry,
|
|
219
|
+
status: ChatSendStatus.Saving,
|
|
220
|
+
retry: undefined,
|
|
221
|
+
};
|
|
201
222
|
this.pendingSaves.set(id, saving);
|
|
202
223
|
this.replace(saving);
|
|
203
224
|
this.persist();
|
|
@@ -221,6 +242,7 @@ export class ChatSendQueue {
|
|
|
221
242
|
else {
|
|
222
243
|
await this.save({
|
|
223
244
|
...current,
|
|
245
|
+
retry: undefined,
|
|
224
246
|
status: current.server_id
|
|
225
247
|
? ChatSendStatus.Confirming
|
|
226
248
|
: ChatSendStatus.Queued,
|
|
@@ -266,6 +288,7 @@ export class ChatSendQueue {
|
|
|
266
288
|
if (stored && !chatSendCanRemove(stored))
|
|
267
289
|
return;
|
|
268
290
|
await this.opts.store.remove(id);
|
|
291
|
+
this.pendingCreates.delete(id);
|
|
269
292
|
this.entries = this.entries.filter((value) => value.id !== id);
|
|
270
293
|
this.publish();
|
|
271
294
|
this.opts.changed?.();
|
|
@@ -290,6 +313,7 @@ export class ChatSendQueue {
|
|
|
290
313
|
if (this.current(owner, signal)) {
|
|
291
314
|
this.error = err;
|
|
292
315
|
this.publish();
|
|
316
|
+
this.recoverStorage();
|
|
293
317
|
}
|
|
294
318
|
},
|
|
295
319
|
on_idle: () => {
|
|
@@ -301,54 +325,65 @@ export class ChatSendQueue {
|
|
|
301
325
|
if (!this.current(owner, signal) || this.cancellations.has(entry.id)) {
|
|
302
326
|
return false;
|
|
303
327
|
}
|
|
304
|
-
|
|
305
|
-
|
|
328
|
+
// A persisted Sending marker is an interrupted request once we own
|
|
329
|
+
// the room lock. Reuse its key; never skip an unknown creation outcome.
|
|
330
|
+
let current = this.pendingCreates.get(entry.id) ?? entry;
|
|
331
|
+
if (this.waitForRetry(current))
|
|
306
332
|
return false;
|
|
307
|
-
}
|
|
308
|
-
else if (entry.status !== ChatSendStatus.Queued) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
let current = entry;
|
|
312
333
|
const request = new AbortController();
|
|
313
334
|
const requestSignal = AbortSignal.any([signal, request.signal]);
|
|
314
335
|
this.sends.set(entry.id, request);
|
|
315
336
|
try {
|
|
316
|
-
current
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
if (!current.server_id) {
|
|
338
|
+
current = {
|
|
339
|
+
...current,
|
|
340
|
+
status: ChatSendStatus.Sending,
|
|
341
|
+
attempted: true,
|
|
342
|
+
};
|
|
343
|
+
await this.save(current);
|
|
344
|
+
if (!this.current(owner, signal))
|
|
345
|
+
return false;
|
|
346
|
+
requestSignal.throwIfAborted();
|
|
347
|
+
const out = await this.opts.chat.insertOneChat({
|
|
348
|
+
in: current.input,
|
|
349
|
+
signal: requestSignal,
|
|
350
|
+
idempotency_key: current.id,
|
|
351
|
+
});
|
|
352
|
+
current = {
|
|
353
|
+
...current,
|
|
354
|
+
server_id: requiredOperationID(out, "Created chat response did not contain an ID."),
|
|
355
|
+
};
|
|
356
|
+
this.pendingCreates.set(current.id, current);
|
|
324
357
|
}
|
|
325
|
-
requestSignal.throwIfAborted();
|
|
326
|
-
const out = await this.opts.chat.insertOneChat({
|
|
327
|
-
in: current.input,
|
|
328
|
-
signal: requestSignal,
|
|
329
|
-
idempotency_key: current.id,
|
|
330
|
-
});
|
|
331
358
|
current = {
|
|
332
359
|
...current,
|
|
333
360
|
status: ChatSendStatus.Confirming,
|
|
334
|
-
|
|
361
|
+
retry: undefined,
|
|
335
362
|
};
|
|
336
363
|
// Persist successful creation before advancing the room's FIFO. A
|
|
337
364
|
// later retrieval failure must never cause this POST to be repeated.
|
|
338
365
|
await this.save(current);
|
|
366
|
+
this.pendingCreates.delete(current.id);
|
|
339
367
|
if (!this.current(owner, signal)) {
|
|
340
368
|
return false;
|
|
341
369
|
}
|
|
342
370
|
this.startConfirmation(current, owner, signal);
|
|
343
371
|
}
|
|
344
372
|
catch (err) {
|
|
373
|
+
current = {
|
|
374
|
+
...current,
|
|
375
|
+
status: requestSignal.aborted
|
|
376
|
+
? ChatSendStatus.Interrupted
|
|
377
|
+
: ChatSendStatus.Failed,
|
|
378
|
+
retry: requestSignal.aborted
|
|
379
|
+
? undefined
|
|
380
|
+
: nextQueueRetry(current.retry),
|
|
381
|
+
};
|
|
382
|
+
if (current.server_id)
|
|
383
|
+
this.pendingCreates.set(current.id, current);
|
|
345
384
|
try {
|
|
346
|
-
await this.save(
|
|
347
|
-
|
|
348
|
-
status: requestSignal.aborted
|
|
349
|
-
? ChatSendStatus.Interrupted
|
|
350
|
-
: ChatSendStatus.Failed,
|
|
351
|
-
});
|
|
385
|
+
await this.save(current);
|
|
386
|
+
this.pendingCreates.delete(current.id);
|
|
352
387
|
}
|
|
353
388
|
catch (storageError) {
|
|
354
389
|
this.error = storageError;
|
|
@@ -356,6 +391,8 @@ export class ChatSendQueue {
|
|
|
356
391
|
if (this.current(owner, signal) && !request.signal.aborted) {
|
|
357
392
|
this.error = err;
|
|
358
393
|
this.publish();
|
|
394
|
+
if (current.retry)
|
|
395
|
+
this.scheduleRecovery(current.retry.after);
|
|
359
396
|
}
|
|
360
397
|
return false;
|
|
361
398
|
}
|
|
@@ -369,7 +406,7 @@ export class ChatSendQueue {
|
|
|
369
406
|
// Retrieval is independent of the room's creation lock. Its per-message
|
|
370
407
|
// lock coordinates retries/recovery across tabs without holding later POSTs.
|
|
371
408
|
startConfirmation(entry, owner, signal) {
|
|
372
|
-
if (this.confirmations.has(entry.id)) {
|
|
409
|
+
if (this.confirmations.has(entry.id) || this.waitForRetry(entry)) {
|
|
373
410
|
return;
|
|
374
411
|
}
|
|
375
412
|
this.confirmations.add(entry.id);
|
|
@@ -379,7 +416,9 @@ export class ChatSendQueue {
|
|
|
379
416
|
return;
|
|
380
417
|
}
|
|
381
418
|
const stored = (await this.opts.store.list(owner)).find((value) => value.id === entry.id);
|
|
382
|
-
if (!stored ||
|
|
419
|
+
if (!stored ||
|
|
420
|
+
!chatSendCanConfirm(stored) ||
|
|
421
|
+
this.waitForRetry(stored)) {
|
|
383
422
|
return;
|
|
384
423
|
}
|
|
385
424
|
let current = stored;
|
|
@@ -406,6 +445,7 @@ export class ChatSendQueue {
|
|
|
406
445
|
...current,
|
|
407
446
|
chat,
|
|
408
447
|
status: ChatSendStatus.UploadInterrupted,
|
|
448
|
+
retry: undefined,
|
|
409
449
|
};
|
|
410
450
|
await this.save(current);
|
|
411
451
|
this.startUpload(current, owner, signal);
|
|
@@ -418,8 +458,15 @@ export class ChatSendQueue {
|
|
|
418
458
|
}
|
|
419
459
|
}
|
|
420
460
|
catch (err) {
|
|
461
|
+
const retry = signal.aborted
|
|
462
|
+
? undefined
|
|
463
|
+
: nextQueueRetry(current.retry);
|
|
464
|
+
if (retry && !current.chat && this.current(owner, signal)) {
|
|
465
|
+
this.scheduleRecovery(retry.after);
|
|
466
|
+
}
|
|
421
467
|
await this.save({
|
|
422
468
|
...current,
|
|
469
|
+
retry,
|
|
423
470
|
status: current.chat
|
|
424
471
|
? ChatSendStatus.UploadInterrupted
|
|
425
472
|
: signal.aborted
|
|
@@ -436,6 +483,7 @@ export class ChatSendQueue {
|
|
|
436
483
|
if (this.current(owner, signal)) {
|
|
437
484
|
this.error = err;
|
|
438
485
|
this.publish();
|
|
486
|
+
this.recoverStorage();
|
|
439
487
|
}
|
|
440
488
|
})
|
|
441
489
|
.finally(() => {
|
|
@@ -563,6 +611,25 @@ export class ChatSendQueue {
|
|
|
563
611
|
})
|
|
564
612
|
.finally(() => this.uploads.delete(entry.id));
|
|
565
613
|
}
|
|
614
|
+
waitForRetry(entry) {
|
|
615
|
+
if (!entry.retry || entry.retry.after <= Date.now())
|
|
616
|
+
return false;
|
|
617
|
+
this.scheduleRecovery(entry.retry.after);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
recoverStorage() {
|
|
621
|
+
this.storageRetry = nextQueueRetry(this.storageRetry);
|
|
622
|
+
this.scheduleRecovery(this.storageRetry.after);
|
|
623
|
+
}
|
|
624
|
+
scheduleRecovery(after) {
|
|
625
|
+
if (!this.enabled)
|
|
626
|
+
return;
|
|
627
|
+
this.wakeup.schedule(after, this.controller.signal, () => {
|
|
628
|
+
// Persist also refreshes the durable queue. It is app-owned, never
|
|
629
|
+
// dependent on a mounted chat tool or a presentation subscription.
|
|
630
|
+
this.persist();
|
|
631
|
+
});
|
|
632
|
+
}
|
|
566
633
|
uploadLock(entry) {
|
|
567
634
|
return `chat-upload:${entry.owner_id}:${entry.id}`;
|
|
568
635
|
}
|
|
@@ -614,7 +681,8 @@ function chatSendCanConfirm(entry) {
|
|
|
614
681
|
(entry.status === ChatSendStatus.Confirming ||
|
|
615
682
|
entry.status === ChatSendStatus.Sending ||
|
|
616
683
|
entry.status === ChatSendStatus.Queued ||
|
|
617
|
-
entry.status === ChatSendStatus.Interrupted
|
|
684
|
+
entry.status === ChatSendStatus.Interrupted ||
|
|
685
|
+
entry.status === ChatSendStatus.Failed));
|
|
618
686
|
}
|
|
619
687
|
export function chatSendNeedsRetry(entry) {
|
|
620
688
|
return (entry.status === ChatSendStatus.Failed ||
|
package/dist/src/core/date.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Date as SchemaDate, InDate } from "@fabriktor/schema";
|
|
2
|
+
export declare const utcTimezone = "UTC";
|
|
2
3
|
export type LocalDateParts = {
|
|
3
4
|
y: number;
|
|
4
5
|
m: number;
|
|
@@ -12,3 +13,6 @@ export declare function zeroDate(): SchemaDate;
|
|
|
12
13
|
export declare function isZeroDate(v: LocalDateParts): boolean;
|
|
13
14
|
export declare function isZeroDateTime(value: string | null | undefined): boolean;
|
|
14
15
|
export declare function isZeroSchemaDate(value: SchemaDate | null | undefined): boolean;
|
|
16
|
+
export declare function dateTimezone(timezone: string): string;
|
|
17
|
+
export declare function inDateAtTimestamp(timestamp: string, timezone?: string): InDate | null;
|
|
18
|
+
export declare function timestampDate(timestamp: string): globalThis.Date | null;
|
package/dist/src/core/date.js
CHANGED
|
@@ -16,7 +16,7 @@ const zeroDatePrefix = [
|
|
|
16
16
|
String(zeroDateParts.d).padStart(2, "0"),
|
|
17
17
|
].join("-");
|
|
18
18
|
const zeroUTC = `${zeroDatePrefix}T00:00:00Z`;
|
|
19
|
-
const utcTimezone = "UTC";
|
|
19
|
+
export const utcTimezone = "UTC";
|
|
20
20
|
export function normalizeInputDate(v) {
|
|
21
21
|
if (isZeroDate(v)) {
|
|
22
22
|
return zeroInputDate();
|
|
@@ -53,3 +53,48 @@ export function isZeroSchemaDate(value) {
|
|
|
53
53
|
value === undefined ||
|
|
54
54
|
(isZeroDate(value) && isZeroDateTime(value.utc)));
|
|
55
55
|
}
|
|
56
|
+
// Use the account/location timezone explicitly; never consult the browser's
|
|
57
|
+
// timezone when interpreting a persisted UTC timestamp.
|
|
58
|
+
export function dateTimezone(timezone) {
|
|
59
|
+
const value = timezone.trim();
|
|
60
|
+
if (!value)
|
|
61
|
+
return utcTimezone;
|
|
62
|
+
try {
|
|
63
|
+
new Intl.DateTimeFormat("en", { timeZone: value });
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return utcTimezone;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function inDateAtTimestamp(timestamp, timezone = utcTimezone) {
|
|
71
|
+
const instant = timestampDate(timestamp);
|
|
72
|
+
if (!instant)
|
|
73
|
+
return null;
|
|
74
|
+
const tz = dateTimezone(timezone);
|
|
75
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
76
|
+
timeZone: tz,
|
|
77
|
+
year: "numeric",
|
|
78
|
+
month: "2-digit",
|
|
79
|
+
day: "2-digit",
|
|
80
|
+
hour: "2-digit",
|
|
81
|
+
minute: "2-digit",
|
|
82
|
+
hourCycle: "h23",
|
|
83
|
+
}).formatToParts(instant);
|
|
84
|
+
const part = (type) => Number(parts.find((value) => value.type === type)?.value);
|
|
85
|
+
return {
|
|
86
|
+
y: part("year"),
|
|
87
|
+
m: part("month"),
|
|
88
|
+
d: part("day"),
|
|
89
|
+
h: part("hour"),
|
|
90
|
+
min: part("minute"),
|
|
91
|
+
tz,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function timestampDate(timestamp) {
|
|
95
|
+
if (isZeroDateTime(timestamp) || !/(?:Z|[+-]\d{2}:\d{2})$/i.test(timestamp)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const instant = new globalThis.Date(timestamp);
|
|
99
|
+
return Number.isFinite(instant.getTime()) ? instant : null;
|
|
100
|
+
}
|
package/dist/src/core/queue.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ export type QueueRecord = {
|
|
|
3
3
|
owner_id: string;
|
|
4
4
|
sequence?: number;
|
|
5
5
|
};
|
|
6
|
+
export type QueueRetry = {
|
|
7
|
+
attempts: number;
|
|
8
|
+
after: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function nextQueueRetry(previous?: QueueRetry): QueueRetry;
|
|
11
|
+
export declare class QueueWakeup {
|
|
12
|
+
private pending?;
|
|
13
|
+
schedule(after: number, signal: AbortSignal, run: () => void): void;
|
|
14
|
+
cancel(): void;
|
|
15
|
+
}
|
|
6
16
|
export interface QueueStore<T extends QueueRecord> {
|
|
7
17
|
add(entry: T): Promise<T>;
|
|
8
18
|
put(entry: T): Promise<void>;
|
package/dist/src/core/queue.js
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
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
|
+
const queueRetryInitialMilliseconds = 1_000;
|
|
7
|
+
const queueRetryMaximumMilliseconds = 30_000;
|
|
8
|
+
// Durable deadlines prevent refreshes and other tabs from bypassing backoff.
|
|
9
|
+
export function nextQueueRetry(previous) {
|
|
10
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
11
|
+
const delay = Math.min(queueRetryMaximumMilliseconds, queueRetryInitialMilliseconds * 2 ** (attempts - 1));
|
|
12
|
+
return { attempts, after: Date.now() + delay };
|
|
13
|
+
}
|
|
14
|
+
// One wakeup for the earliest pending recovery. A paused account must never
|
|
15
|
+
// be restarted by an old timer. The caller re-reads durable state on wakeup.
|
|
16
|
+
export class QueueWakeup {
|
|
17
|
+
pending;
|
|
18
|
+
schedule(after, signal, run) {
|
|
19
|
+
if (signal.aborted || (this.pending && this.pending.after <= after))
|
|
20
|
+
return;
|
|
21
|
+
this.cancel();
|
|
22
|
+
const cancel = () => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
signal.removeEventListener("abort", cancel);
|
|
25
|
+
this.pending = undefined;
|
|
26
|
+
};
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
cancel();
|
|
29
|
+
if (!signal.aborted)
|
|
30
|
+
run();
|
|
31
|
+
}, Math.max(0, after - Date.now()));
|
|
32
|
+
this.pending = { after, cancel };
|
|
33
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
34
|
+
}
|
|
35
|
+
cancel() {
|
|
36
|
+
this.pending?.cancel();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
6
39
|
// Store order is authoritative across views/tabs. Each scope has one worker;
|
|
7
40
|
// run must durably retire the head before returning true. False pauses the
|
|
8
41
|
// lane without blocking other scopes. Storage/transport failures never skip it.
|
|
@@ -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[];
|
package/dist/src/core/search.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
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";
|
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
|
|
2
|
-
import type
|
|
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>;
|
package/dist/src/memos/memos.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
-
...
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.106",
|
|
4
4
|
"description": "",
|
|
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.
|
|
20
|
+
"@fabriktor/client": "0.0.83",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
22
|
"@fabriktor/schema": "0.0.63"
|
|
23
23
|
}
|