@fabriktor/fx 0.0.151 → 0.0.153

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.
@@ -3,11 +3,18 @@
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 { PullTypeBatch, PullTypeKeepAlive, PullTypeResearch, } from "@fabriktor/schema";
7
- import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
8
- import { requiredOperationID } from "../core/id.js";
9
- import { newPullSession, } from "../core/pull.js";
6
+ import { isClientErr, queryParams, } from "@fabriktor/client";
7
+ import { PullTypeBatch, PullTypeKeepAlive, PullTypeResearch, PullTypeSingle, OwnerIDKey, } from "@fabriktor/schema";
8
+ import { errRequired, errResolveFailed, errValidation, errUnconfirmedMutation, } from "../core/err.js";
9
+ import { requiredOperationID, resolvedID } from "../core/id.js";
10
+ import { startPullSession, PullSessionScope, } from "../core/pull.js";
10
11
  import { runOwnerSearch, } from "../core/search.js";
12
+ import { createEvent, EventBus, notifyObserver } from "../core/events.js";
13
+ import { marketplacePostInput, prepareMarketplacePost, validateMarketplaceUserType, } from "./validation.js";
14
+ export * from "./validation.js";
15
+ export const MarketplaceEvents = {
16
+ Changed: createEvent(),
17
+ };
11
18
  const newMarketplaceSessionToken = "";
12
19
  const resourceMarketplaceSessionToken = "marketplace_session_token";
13
20
  const resourceMarketplacePost = "marketplace_post";
@@ -25,25 +32,118 @@ const msgMarketplaceEntityIDRequired = "Entity ID is required for a batch market
25
32
  const msgMarketplaceResearchRequired = "Research is required for a research marketplace session.";
26
33
  const msgMarketplaceCountryRequired = "Country is required for marketplace welcome research.";
27
34
  export class MarketplaceFX {
35
+ sessions = new PullSessionScope();
36
+ events;
37
+ mutating = false;
28
38
  marketplace;
29
39
  keep_alive_interval_ms;
30
40
  constructor(opts) {
31
41
  validateMarketplaceFXOptions(opts);
32
42
  this.marketplace = opts.marketplace;
43
+ this.events = opts.events ?? new EventBus();
33
44
  this.keep_alive_interval_ms = opts.keep_alive_interval_ms;
34
45
  }
46
+ reset() {
47
+ this.sessions.reset();
48
+ }
35
49
  async createMarketplacePost(opts) {
36
- const out = await this.marketplace.insertOneMarketplacePost(opts);
37
- const id = requiredOperationID(out, msgCreatedMarketplacePostIDMissing);
38
- return await this.findMarketplacePost(id);
50
+ return await this.mutate(async (signal) => {
51
+ let id = resolvedID(opts.created_id);
52
+ if (!id) {
53
+ if (opts.attempted)
54
+ throw errUnconfirmedMutation();
55
+ const input = prepareMarketplacePost(opts.in);
56
+ opts.before_request?.();
57
+ let out;
58
+ try {
59
+ out = await this.marketplace.insertOneMarketplacePost({
60
+ in: input,
61
+ idempotency_key: opts.idempotency_key,
62
+ signal,
63
+ });
64
+ }
65
+ catch (error) {
66
+ // Validation and moderation reject before insertion. Other failures may
67
+ // have committed; keep the original request immutable for an explicit retry.
68
+ if (isClientErr(error) &&
69
+ (error.status === 400 || error.status === 422))
70
+ notifyObserver(() => opts.on_rejected?.());
71
+ throw error;
72
+ }
73
+ id = requiredOperationID(out, msgCreatedMarketplacePostIDMissing);
74
+ notifyObserver(() => opts.on_created?.(id));
75
+ }
76
+ signal.throwIfAborted();
77
+ const post = await this.findMarketplacePost(id, signal);
78
+ signal.throwIfAborted();
79
+ this.changed(post.owner_id, "posts", id);
80
+ return post;
81
+ }, opts.signal);
39
82
  }
40
83
  async replaceMarketplacePost(opts) {
41
- await this.marketplace.replaceOneMarketplacePost(opts);
42
- return await this.findMarketplacePost(opts.id);
84
+ return await this.mutate(async (signal) => {
85
+ if (opts.created_id) {
86
+ if (opts.created_id !== opts.id)
87
+ throw errValidation({
88
+ field: "id",
89
+ msg: "The saved listing does not match this edit.",
90
+ });
91
+ const post = await this.findMarketplacePost(opts.id, signal);
92
+ if (opts.expected && post.owner_id !== opts.expected.owner_id)
93
+ throw errValidation({
94
+ field: "owner_id",
95
+ msg: "This listing belongs to another owner.",
96
+ });
97
+ signal.throwIfAborted();
98
+ this.changed(post.owner_id, "posts", post.id);
99
+ return post;
100
+ }
101
+ if (opts.attempted)
102
+ throw errUnconfirmedMutation();
103
+ const current = await this.findMarketplacePost(opts.id, signal);
104
+ signal.throwIfAborted();
105
+ if (opts.expected &&
106
+ (current.owner_id !== opts.expected.owner_id ||
107
+ current.updated_at !== opts.expected.updated_at)) {
108
+ throw errValidation({
109
+ field: "updated_at",
110
+ msg: "Reload the listing before saving it.",
111
+ });
112
+ }
113
+ const input = prepareMarketplacePost({
114
+ ...marketplacePostInput(current),
115
+ ...opts.in,
116
+ });
117
+ opts.before_request?.();
118
+ try {
119
+ await this.marketplace.replaceOneMarketplacePost({
120
+ id: opts.id,
121
+ in: input,
122
+ signal,
123
+ idempotency_key: opts.idempotency_key,
124
+ });
125
+ }
126
+ catch (error) {
127
+ if (isClientErr(error) &&
128
+ (error.status === 400 || error.status === 422))
129
+ notifyObserver(() => opts.on_rejected?.());
130
+ throw error;
131
+ }
132
+ notifyObserver(() => opts.on_created?.(opts.id));
133
+ signal.throwIfAborted();
134
+ this.changed(current.owner_id, "posts", current.id);
135
+ const post = await this.findMarketplacePost(opts.id, signal);
136
+ signal.throwIfAborted();
137
+ return post;
138
+ }, opts.signal);
43
139
  }
44
140
  async searchMarketplacePosts(opts) {
45
141
  return await runOwnerSearch({
46
142
  ...opts,
143
+ query: queryParams({
144
+ post_type: opts.post_type,
145
+ user_type: opts.user_type,
146
+ }),
47
147
  search: (searchOpts) => this.marketplace.searchManyMarketplacePosts(searchOpts),
48
148
  });
49
149
  }
@@ -57,28 +157,30 @@ export class MarketplaceFX {
57
157
  return await this.findMarketplaceFavorite(opts.id);
58
158
  }
59
159
  async startSession(opts) {
60
- const { on_error, ...input } = opts;
160
+ const { on_error, signal: parentSignal, ...input } = opts;
161
+ const signal = this.sessions.signal(parentSignal);
61
162
  const context = marketplaceSessionContext(input);
62
- const initial = await this.marketplace.pull({
63
- ...context,
64
- session_token: newMarketplaceSessionToken,
65
- is_new_session: true,
66
- });
67
- const session_token = marketplaceSessionToken(initial);
68
- return newPullSession({
69
- initial,
70
- session_token,
163
+ return await startPullSession({
164
+ signal,
165
+ pull_initial: (signal) => this.marketplace.pull({
166
+ ...context,
167
+ session_token: newMarketplaceSessionToken,
168
+ is_new_session: true,
169
+ signal,
170
+ }),
71
171
  keep_alive_interval_ms: this.keep_alive_interval_ms,
72
- pull_next: async (token) => {
172
+ pull_next: async (token, signal) => {
73
173
  return await this.marketplace.pull({
74
174
  ...context,
75
175
  session_token: token,
176
+ signal,
76
177
  is_new_session: false,
77
178
  });
78
179
  },
79
- keep_alive: async (token) => {
180
+ keep_alive: async (token, signal) => {
80
181
  return await this.marketplace.pull({
81
182
  session_token: token,
183
+ signal,
82
184
  is_new_session: false,
83
185
  pull_type: PullTypeKeepAlive,
84
186
  entity_id: "",
@@ -104,12 +206,163 @@ export class MarketplaceFX {
104
206
  research,
105
207
  research_user_type: opts.research_user_type,
106
208
  on_error: opts.on_error,
209
+ signal: opts.signal,
107
210
  });
108
211
  }
109
- async findMarketplacePost(id) {
110
- const out = await this.marketplace.findOneMarketplacePost({
111
- id,
212
+ // Shared discoveries are read through pull; generic find-by-ID is owner-only.
213
+ async readMarketplacePost(opts) {
214
+ const signal = this.sessions.signal(opts.signal);
215
+ signal.throwIfAborted();
216
+ const id = this.requireID(opts.id);
217
+ try {
218
+ const out = await this.marketplace.pull({
219
+ session_token: newMarketplaceSessionToken,
220
+ is_new_session: true,
221
+ pull_type: PullTypeSingle,
222
+ entity_id: id,
223
+ research: "",
224
+ research_user_type: "",
225
+ signal,
226
+ });
227
+ signal.throwIfAborted();
228
+ return out.value?.v.find((post) => post.id === id) ?? null;
229
+ }
230
+ catch (error) {
231
+ signal.throwIfAborted();
232
+ if (isClientErr(error) && (error.status === 403 || error.status === 404))
233
+ return null;
234
+ throw error;
235
+ }
236
+ }
237
+ async listMarketplaceFavorites(opts) {
238
+ const signal = this.sessions.signal(opts.signal);
239
+ signal.throwIfAborted();
240
+ const out = await this.marketplace.queryMarketplaceFavorites({
241
+ query: queryParams({
242
+ [OwnerIDKey]: this.requireID(opts.owner_id),
243
+ post_id: opts.post_id === undefined ? undefined : this.requireID(opts.post_id),
244
+ }),
245
+ signal,
246
+ });
247
+ signal.throwIfAborted();
248
+ return out.value ?? [];
249
+ }
250
+ // Favorites have an unpaged query route. Preserve its order and resolve each
251
+ // public listing through pull, including unavailable entries for removal.
252
+ async listSavedMarketplacePosts(opts) {
253
+ const signal = this.sessions.signal(opts.signal);
254
+ const favorites = await this.listMarketplaceFavorites({ ...opts, signal });
255
+ const results = [];
256
+ for (const favorite of favorites) {
257
+ signal.throwIfAborted();
258
+ const post = await this.readMarketplacePost({
259
+ id: favorite.post_id,
260
+ signal,
261
+ });
262
+ signal.throwIfAborted();
263
+ results.push({ ...favorite, post });
264
+ }
265
+ return results;
266
+ }
267
+ // Recheck persisted favorites before creating or removing; clean old duplicates serially.
268
+ async setMarketplaceFavorite(opts) {
269
+ return await this.mutate(async (signal) => {
270
+ const existing = await this.listMarketplaceFavorites({ ...opts, signal });
271
+ if (opts.saved) {
272
+ if (existing.length)
273
+ return existing[0];
274
+ if (!(await this.readMarketplacePost({ id: opts.post_id, signal }))) {
275
+ throw errResolveFailed({
276
+ resource: resourceMarketplacePost,
277
+ msg: msgMarketplacePostMissing,
278
+ });
279
+ }
280
+ const out = await this.marketplace.insertOneMarketplaceFavorite({
281
+ in: { post_id: opts.post_id },
282
+ signal,
283
+ });
284
+ signal.throwIfAborted();
285
+ const id = requiredOperationID(out, msgCreatedMarketplaceFavoriteIDMissing);
286
+ const found = await this.marketplace.findOneMarketplaceFavorite({
287
+ id,
288
+ signal,
289
+ });
290
+ signal.throwIfAborted();
291
+ if (!found.value)
292
+ throw errResolveFailed({
293
+ resource: resourceMarketplaceFavorite,
294
+ msg: msgMarketplaceFavoriteMissing,
295
+ });
296
+ this.changed(opts.owner_id, "favorites", opts.post_id);
297
+ return found.value;
298
+ }
299
+ for (const favorite of existing) {
300
+ signal.throwIfAborted();
301
+ await this.marketplace.deleteOneMarketplaceFavorite({
302
+ id: favorite.id,
303
+ signal,
304
+ });
305
+ }
306
+ signal.throwIfAborted();
307
+ this.changed(opts.owner_id, "favorites", opts.post_id);
308
+ return null;
309
+ }, opts.signal);
310
+ }
311
+ async deleteMarketplacePost(opts) {
312
+ return await this.mutate(async (signal) => {
313
+ const owner = this.requireID(opts.owner_id);
314
+ const current = await this.findMarketplacePost(opts.record.id, signal);
315
+ signal.throwIfAborted();
316
+ if (current.owner_id !== owner ||
317
+ current.updated_at !== opts.record.updated_at) {
318
+ throw errValidation({
319
+ field: "updated_at",
320
+ msg: "Reload the listing before deleting it.",
321
+ });
322
+ }
323
+ await this.marketplace.deleteOneMarketplacePost({
324
+ id: current.id,
325
+ signal,
326
+ });
327
+ signal.throwIfAborted();
328
+ this.changed(owner, "posts", current.id);
112
329
  });
330
+ }
331
+ async mutate(fn, parent) {
332
+ if (this.mutating)
333
+ throw errValidation({
334
+ msg: "A Marketplace action is already in progress.",
335
+ });
336
+ const signal = this.sessions.signal(parent);
337
+ signal.throwIfAborted();
338
+ this.mutating = true;
339
+ try {
340
+ const result = await fn(signal);
341
+ signal.throwIfAborted();
342
+ return result;
343
+ }
344
+ finally {
345
+ this.mutating = false;
346
+ }
347
+ }
348
+ requireID(value) {
349
+ const id = resolvedID(value);
350
+ if (!id)
351
+ throw errRequired({
352
+ field: "id",
353
+ msg: "A Marketplace record or owner ID is required.",
354
+ });
355
+ return id;
356
+ }
357
+ changed(owner_id, resource, post_id) {
358
+ this.events.publish(MarketplaceEvents.Changed, {
359
+ owner_id,
360
+ resource,
361
+ post_id,
362
+ });
363
+ }
364
+ async findMarketplacePost(id, signal) {
365
+ const out = await this.marketplace.findOneMarketplacePost({ id, signal });
113
366
  const post = out.value;
114
367
  if (post === undefined) {
115
368
  throw errResolveFailed({
@@ -170,6 +423,7 @@ function marketplaceSessionContext(opts) {
170
423
  msg: msgMarketplaceResearchRequired,
171
424
  });
172
425
  }
426
+ validateMarketplaceUserType(opts.research_user_type);
173
427
  return {
174
428
  pull_type: PullTypeResearch,
175
429
  entity_id: "",
@@ -0,0 +1,11 @@
1
+ import { PostTypeEmployment, PostTypeJob, type InMarketplacePost, type MarketplacePost } from "@fabriktor/schema";
2
+ export type MarketplacePostType = typeof PostTypeEmployment | typeof PostTypeJob;
3
+ export declare const marketplaceUserTypes: readonly ["client", "employee", "employer"];
4
+ export declare const MarketplaceResource: {
5
+ readonly Posts: "marketplace_posts";
6
+ readonly Favorites: "marketplace_favorites";
7
+ };
8
+ export declare function marketplacePostTypes(user_type: string): readonly MarketplacePostType[];
9
+ export declare function validateMarketplaceUserType(user_type: string): void;
10
+ export declare function marketplacePostInput(post: MarketplacePost): InMarketplacePost;
11
+ export declare function prepareMarketplacePost(input: InMarketplacePost): InMarketplacePost;
@@ -0,0 +1,76 @@
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 { ColMarketplaceMarketplacePosts, ColMarketplaceMarketplaceFavorites, PostTypeEmployment, PostTypeJob, UserTypeClient, UserTypeEmployee, UserTypeEmployer, } from "@fabriktor/schema";
7
+ import { errRequired, errValidation } from "../core/err.js";
8
+ export const marketplaceUserTypes = [
9
+ UserTypeClient,
10
+ UserTypeEmployee,
11
+ UserTypeEmployer,
12
+ ];
13
+ export const MarketplaceResource = {
14
+ Posts: ColMarketplaceMarketplacePosts,
15
+ Favorites: ColMarketplaceMarketplaceFavorites,
16
+ };
17
+ export function marketplacePostTypes(user_type) {
18
+ switch (user_type) {
19
+ case UserTypeEmployer:
20
+ return [PostTypeEmployment, PostTypeJob];
21
+ case UserTypeEmployee:
22
+ return [PostTypeEmployment];
23
+ case UserTypeClient:
24
+ return [PostTypeJob];
25
+ default:
26
+ return [];
27
+ }
28
+ }
29
+ export function validateMarketplaceUserType(user_type) {
30
+ if (!marketplaceUserTypes.some((value) => value === user_type)) {
31
+ throw errValidation({
32
+ field: "user_type",
33
+ msg: "A Marketplace account role is required.",
34
+ });
35
+ }
36
+ }
37
+ export function marketplacePostInput(post) {
38
+ return {
39
+ text: { text: post.text.text, file: "", file_url: "", mime: "" },
40
+ post_type: post.post_type,
41
+ user_type: post.user_type,
42
+ country: post.country,
43
+ state: post.state,
44
+ city: post.city,
45
+ };
46
+ }
47
+ // Project only editable fields. Embeddings, ownership and normalization stay server-owned.
48
+ export function prepareMarketplacePost(input) {
49
+ validateMarketplaceUserType(input.user_type);
50
+ if (!marketplacePostTypes(input.user_type).some((value) => value === input.post_type)) {
51
+ throw errValidation({
52
+ field: "post_type",
53
+ msg: "This listing purpose is unavailable for the selected role.",
54
+ });
55
+ }
56
+ const text = input.text.text.trim();
57
+ if (!text)
58
+ throw errRequired({
59
+ field: "text",
60
+ msg: "A listing description is required.",
61
+ });
62
+ const country = input.country.trim().toUpperCase();
63
+ if (!country)
64
+ throw errRequired({
65
+ field: "country",
66
+ msg: "A listing country is required.",
67
+ });
68
+ return {
69
+ text: { text, file: "", file_url: "", mime: "" },
70
+ post_type: input.post_type,
71
+ user_type: input.user_type,
72
+ country,
73
+ state: input.state.trim(),
74
+ city: input.city.trim(),
75
+ };
76
+ }
@@ -29,6 +29,10 @@ export declare const RoutePlanningBlock: {
29
29
  readonly DepartureChanged: "route_departure_changed";
30
30
  readonly DestinationTimezone: "route_destination_timezone";
31
31
  readonly DestinationChanged: "route_destination_changed";
32
+ readonly DestinationOwner: "route_destination_owner";
33
+ readonly DestinationArchived: "route_destination_archived";
34
+ readonly DestinationAddress: "route_destination_address";
35
+ readonly DestinationClient: "route_destination_client";
32
36
  };
33
37
  export declare function companyRouteDeparture(user: User): RouteDeparture;
34
38
  export declare function warehouseRouteDeparture(warehouse: Warehouse, owner_id: string): RouteDeparture;
@@ -12,6 +12,10 @@ export const RoutePlanningBlock = {
12
12
  DepartureChanged: "route_departure_changed",
13
13
  DestinationTimezone: "route_destination_timezone",
14
14
  DestinationChanged: "route_destination_changed",
15
+ DestinationOwner: "route_destination_owner",
16
+ DestinationArchived: "route_destination_archived",
17
+ DestinationAddress: "route_destination_address",
18
+ DestinationClient: "route_destination_client",
15
19
  };
16
20
  export function companyRouteDeparture(user) {
17
21
  if (user.account_identity !== UserTypeEmployer ||
@@ -57,11 +61,14 @@ export async function loadRouteDeparture(readers, owner_id, reference, signal) {
57
61
  throw new PreActionBlocked(RoutePlanningBlock.Departure);
58
62
  }
59
63
  export function validateRoutePlanLocation(location, owner_id, timezone) {
60
- if (location.owner_id !== owner_id ||
61
- location.is_archived ||
62
- !location.place_id.trim() ||
63
- !resolvedID(location.client_id))
64
- throw new PreActionBlocked(RoutePlanningBlock.DestinationChanged);
64
+ if (location.owner_id !== owner_id)
65
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationOwner);
66
+ if (location.is_archived)
67
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationArchived);
68
+ if (!location.place_id?.trim())
69
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationAddress);
70
+ if (!resolvedID(location.client_id))
71
+ throw new PreActionBlocked(RoutePlanningBlock.DestinationClient);
65
72
  if (addressTimezone(location) !== timezone)
66
73
  throw new PreActionBlocked(RoutePlanningBlock.DestinationTimezone);
67
74
  }
@@ -1,5 +1,7 @@
1
1
  import { type Object as StorageObject, type RPStorageUpload } from "@fabriktor/schema";
2
2
  import type { OperationResultOf, RequestOptions, OptionalField, StorageCollection, StorageOperator, StorageUploadFile, UploadStorageOptions } from "@fabriktor/client";
3
+ export declare const storageMaxFiles = 20;
4
+ export declare const storageMaxBytes: number;
3
5
  type StorageDownloadOptions = Parameters<StorageOperator["download"]>[1];
4
6
  export declare const StorageUploadModeration: {
5
7
  readonly Clean: "clean";
@@ -20,6 +22,8 @@ export type UploadObjectsOptions = UploadStorageOptions & {
20
22
  col: StorageCollection;
21
23
  };
22
24
  export type UploadObjectsWithPersonalBoxOptions = UploadObjectsOptions & {
25
+ primary?: StorageUploadResult;
26
+ on_primary?: (result: StorageUploadResult) => void;
23
27
  personal_box_idempotency_key?: string;
24
28
  user_id?: string;
25
29
  save_to_personal_box?: boolean;
@@ -27,6 +31,13 @@ export type UploadObjectsWithPersonalBoxOptions = UploadObjectsOptions & {
27
31
  export type DownloadObjectsOptions = OptionalField<StorageDownloadOptions, "mime_prefix"> & {
28
32
  col: StorageCollection;
29
33
  };
34
+ export type DeleteOwnedStorageObjectOptions = {
35
+ col: StorageCollection;
36
+ id: string;
37
+ owner_id: string;
38
+ uploader_id: string;
39
+ signal?: AbortSignal;
40
+ };
30
41
  export type UploadUserObjectsOptions = RequestOptions & {
31
42
  user_id: string;
32
43
  uploader_id?: string;
@@ -62,6 +73,7 @@ export type StoragePersonalBoxUploadResult = {
62
73
  };
63
74
  export type UploadChatObjectsResult = StoragePersonalBoxUploadResult;
64
75
  export interface StorageFXOperator {
76
+ deleteOwnedObject(opts: DeleteOwnedStorageObjectOptions): Promise<void>;
65
77
  uploadObjects(opts: UploadObjectsOptions): Promise<StorageUploadResult>;
66
78
  uploadObjectsWithPersonalBox(opts: UploadObjectsWithPersonalBoxOptions): Promise<StoragePersonalBoxUploadResult>;
67
79
  downloadObjects(opts: DownloadObjectsOptions): Promise<OperationResultOf<StorageObject[]>>;
@@ -74,8 +86,10 @@ export declare class StorageFX implements StorageFXOperator {
74
86
  uploadObjects(opts: UploadObjectsOptions): Promise<StorageUploadResult>;
75
87
  uploadObjectsWithPersonalBox(opts: UploadObjectsWithPersonalBoxOptions): Promise<StoragePersonalBoxUploadResult>;
76
88
  downloadObjects(opts: DownloadObjectsOptions): Promise<OperationResultOf<StorageObject[]>>;
89
+ deleteOwnedObject(opts: DeleteOwnedStorageObjectOptions): Promise<void>;
77
90
  uploadUserObjects(opts: UploadUserObjectsOptions): Promise<StorageUploadResult>;
78
91
  uploadChatObjects(opts: UploadChatObjectsOptions): Promise<UploadChatObjectsResult>;
79
92
  }
80
93
  export declare function newStorageFX(opts: StorageFXOptions): StorageFX;
94
+ export declare function validateStorageUploadFiles(files: StorageUploadFile[]): void;
81
95
  export {};
@@ -4,9 +4,11 @@
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 { ColStorageChat, ColStorageUsers, } from "@fabriktor/schema";
7
+ import { isClientErr } from "@fabriktor/client";
7
8
  import { errRequired, errValidation } from "../core/err.js";
8
- const maxUploadFiles = 20;
9
- const maxUploadBytes = 1024 * 1024 * 1024;
9
+ import { notifyObserver } from "../core/events.js";
10
+ export const storageMaxFiles = 20;
11
+ export const storageMaxBytes = 1024 * 1024 * 1024;
10
12
  const mimePrefixAll = "";
11
13
  const extensionHEIC = ".heic";
12
14
  const extensionHEIF = ".heif";
@@ -53,17 +55,20 @@ export class StorageFX {
53
55
  }
54
56
  async uploadObjectsWithPersonalBox(opts) {
55
57
  const personal_box_user_id = personalBoxUserID(opts);
56
- const primary = await this.uploadObjects({
57
- col: opts.col,
58
- owner_id: opts.owner_id,
59
- uploader_id: opts.uploader_id,
60
- full_quality: opts.full_quality,
61
- public_url: opts.public_url,
62
- files: opts.files,
63
- signal: opts.signal,
64
- idempotency_key: opts.idempotency_key,
65
- });
66
- if (personal_box_user_id === undefined) {
58
+ opts.signal?.throwIfAborted();
59
+ const primary = opts.primary ??
60
+ (await this.uploadObjects({
61
+ col: opts.col,
62
+ owner_id: opts.owner_id,
63
+ uploader_id: opts.uploader_id,
64
+ full_quality: opts.full_quality,
65
+ public_url: opts.public_url,
66
+ files: opts.files,
67
+ signal: opts.signal,
68
+ idempotency_key: opts.idempotency_key,
69
+ }));
70
+ notifyObserver(() => opts.on_primary?.(primary));
71
+ if (personal_box_user_id === undefined || primary.all_forbidden) {
67
72
  return {
68
73
  status: StoragePersonalBoxUploadStatus.PrimaryOnly,
69
74
  primary,
@@ -108,6 +113,32 @@ export class StorageFX {
108
113
  mime_prefix: mime_prefix?.trim() ?? mimePrefixAll,
109
114
  });
110
115
  }
116
+ async deleteOwnedObject(opts) {
117
+ opts.signal?.throwIfAborted();
118
+ const stored = await this.downloadObjects(opts);
119
+ opts.signal?.throwIfAborted();
120
+ const object = stored.value?.find((value) => value.id === opts.id);
121
+ if (!object)
122
+ return;
123
+ if (!opts.uploader_id.trim() ||
124
+ object.owner_id !== opts.owner_id ||
125
+ object.uploader_id !== opts.uploader_id)
126
+ throw errValidation({
127
+ field: "owner_id",
128
+ msg: "Storage object does not belong to this owner and uploader.",
129
+ });
130
+ try {
131
+ await this.storage.deleteOne(opts.col, {
132
+ id: object.id,
133
+ signal: opts.signal,
134
+ });
135
+ }
136
+ catch (error) {
137
+ if (!isClientErr(error) || error.status !== 404)
138
+ throw error;
139
+ }
140
+ opts.signal?.throwIfAborted();
141
+ }
111
142
  async uploadUserObjects(opts) {
112
143
  const user_id = opts.user_id.trim();
113
144
  if (user_id === "") {
@@ -171,25 +202,28 @@ function validateUpload(opts) {
171
202
  msg: msgOwnerIDRequired,
172
203
  });
173
204
  }
174
- if (opts.files.length === 0) {
205
+ validateStorageUploadFiles(opts.files);
206
+ }
207
+ export function validateStorageUploadFiles(files) {
208
+ if (files.length === 0) {
175
209
  throw errRequired({
176
210
  field: "files",
177
211
  msg: msgUploadFilesRequired,
178
212
  });
179
213
  }
180
- if (opts.files.length > maxUploadFiles) {
214
+ if (files.length > storageMaxFiles) {
181
215
  throw errValidation({
182
216
  field: "files",
183
217
  msg: msgUploadTooManyFiles,
184
218
  });
185
219
  }
186
- if (uploadSize(opts.files) > maxUploadBytes) {
220
+ if (uploadSize(files) > storageMaxBytes) {
187
221
  throw errValidation({
188
222
  field: "files",
189
223
  msg: msgUploadTooLarge,
190
224
  });
191
225
  }
192
- if (opts.files.some(isHEIC)) {
226
+ if (files.some(isHEIC)) {
193
227
  throw errValidation({
194
228
  field: "files",
195
229
  msg: msgHEICUnsupported,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.151",
3
+ "version": "0.0.153",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,9 +17,9 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.99",
20
+ "@fabriktor/client": "0.0.100",
21
21
  "@fabriktor/fx": "0.0.94",
22
- "@fabriktor/schema": "0.0.69",
22
+ "@fabriktor/schema": "0.0.70",
23
23
  "convert": "8.0.2"
24
24
  }
25
25
  }