@fabriktor/fx 0.0.152 → 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.
- package/dist/src/core/err.d.ts +2 -0
- package/dist/src/core/err.js +10 -0
- package/dist/src/core/multi-create.d.ts +1 -0
- package/dist/src/core/multi-create.js +1 -0
- package/dist/src/core/pull.d.ts +27 -2
- package/dist/src/core/pull.js +108 -6
- package/dist/src/core/search-controller.d.ts +1 -0
- package/dist/src/core/search-controller.js +1 -0
- package/dist/src/feed/feed.d.ts +71 -7
- package/dist/src/feed/feed.js +454 -302
- package/dist/src/feed/media.d.ts +53 -0
- package/dist/src/feed/media.js +354 -0
- package/dist/src/feed/validation.d.ts +4 -0
- package/dist/src/feed/validation.js +59 -0
- package/dist/src/fx.js +2 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/marketplace/marketplace.d.ts +64 -5
- package/dist/src/marketplace/marketplace.js +278 -24
- package/dist/src/marketplace/validation.d.ts +11 -0
- package/dist/src/marketplace/validation.js +76 -0
- package/dist/src/storage/storage.d.ts +14 -0
- package/dist/src/storage/storage.js +51 -17
- package/package.json +3 -3
|
@@ -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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
42
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
9
|
-
const
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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 (
|
|
214
|
+
if (files.length > storageMaxFiles) {
|
|
181
215
|
throw errValidation({
|
|
182
216
|
field: "files",
|
|
183
217
|
msg: msgUploadTooManyFiles,
|
|
184
218
|
});
|
|
185
219
|
}
|
|
186
|
-
if (uploadSize(
|
|
220
|
+
if (uploadSize(files) > storageMaxBytes) {
|
|
187
221
|
throw errValidation({
|
|
188
222
|
field: "files",
|
|
189
223
|
msg: msgUploadTooLarge,
|
|
190
224
|
});
|
|
191
225
|
}
|
|
192
|
-
if (
|
|
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.
|
|
3
|
+
"version": "0.0.153",
|
|
4
4
|
"description": "",
|
|
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.
|
|
20
|
+
"@fabriktor/client": "0.0.100",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
|
-
"@fabriktor/schema": "0.0.
|
|
22
|
+
"@fabriktor/schema": "0.0.70",
|
|
23
23
|
"convert": "8.0.2"
|
|
24
24
|
}
|
|
25
25
|
}
|