@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type FeedOperator, type StorageUploadFile } from "@fabriktor/client";
|
|
2
|
+
import { type FeedPost, type InFeedPost, type FeedObject, type Object as StorageObject } from "@fabriktor/schema";
|
|
3
|
+
import type { MultiCreateAttempt } from "../core/multi-create.js";
|
|
4
|
+
import { type StorageFXOperator, type StoragePersonalBoxUploadResult, type StorageUploadResult } from "../storage/storage.js";
|
|
5
|
+
export type FeedPostDraft = {
|
|
6
|
+
in: InFeedPost;
|
|
7
|
+
files: StorageUploadFile[];
|
|
8
|
+
full_quality: boolean;
|
|
9
|
+
save_to_personal_box: boolean;
|
|
10
|
+
retained_urls: string[];
|
|
11
|
+
};
|
|
12
|
+
export type FeedSaveStage = "create" | "upload" | "resolve" | "publish" | "cleanup";
|
|
13
|
+
export type FeedPostSaveResult = {
|
|
14
|
+
post: FeedPost | null;
|
|
15
|
+
upload?: StoragePersonalBoxUploadResult;
|
|
16
|
+
};
|
|
17
|
+
export type FeedPostSaveState = {
|
|
18
|
+
post_id?: string;
|
|
19
|
+
create_attempted?: boolean;
|
|
20
|
+
upload_attempted?: boolean;
|
|
21
|
+
original?: FeedPost;
|
|
22
|
+
baseline?: StorageObject[];
|
|
23
|
+
primary?: StorageUploadResult;
|
|
24
|
+
upload?: StoragePersonalBoxUploadResult;
|
|
25
|
+
input?: InFeedPost & {
|
|
26
|
+
objects: FeedObject[];
|
|
27
|
+
};
|
|
28
|
+
patch_attempted?: boolean;
|
|
29
|
+
patched?: boolean;
|
|
30
|
+
rejected?: boolean;
|
|
31
|
+
removed?: StorageObject[];
|
|
32
|
+
readonly create_key: string;
|
|
33
|
+
readonly upload_key: string;
|
|
34
|
+
readonly personal_key: string;
|
|
35
|
+
readonly patch_key: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function newFeedPostSaveState(): FeedPostSaveState;
|
|
38
|
+
export type SaveFeedPostOptions = MultiCreateAttempt & {
|
|
39
|
+
draft: FeedPostDraft;
|
|
40
|
+
state: FeedPostSaveState;
|
|
41
|
+
owner_id?: string;
|
|
42
|
+
expected?: FeedPost;
|
|
43
|
+
on_stage?: (stage: FeedSaveStage) => void;
|
|
44
|
+
};
|
|
45
|
+
type SaveDependencies = {
|
|
46
|
+
feed: FeedOperator;
|
|
47
|
+
storage: StorageFXOperator;
|
|
48
|
+
read: (id: string, signal: AbortSignal) => Promise<FeedPost>;
|
|
49
|
+
changed: (id: string) => void;
|
|
50
|
+
};
|
|
51
|
+
export declare function saveFeedPost(deps: SaveDependencies, opts: SaveFeedPostOptions): Promise<FeedPostSaveResult>;
|
|
52
|
+
export declare function feedObjects(objects: StorageObject[]): FeedObject[];
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,354 @@
|
|
|
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 { newIdempotencyKey, isClientErr, } from "@fabriktor/client";
|
|
7
|
+
import { All, ColStorageFeed, ZeroID, } from "@fabriktor/schema";
|
|
8
|
+
import { errResolveFailed, errValidation, errUnconfirmedMutation, } from "../core/err.js";
|
|
9
|
+
import { notifyObserver } from "../core/events.js";
|
|
10
|
+
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
11
|
+
import { validateStorageUploadFiles, } from "../storage/storage.js";
|
|
12
|
+
import { feedPostInput, validateFeedVisibility } from "./validation.js";
|
|
13
|
+
export function newFeedPostSaveState() {
|
|
14
|
+
return {
|
|
15
|
+
create_key: newIdempotencyKey(),
|
|
16
|
+
upload_key: newIdempotencyKey(),
|
|
17
|
+
personal_key: newIdempotencyKey(),
|
|
18
|
+
patch_key: newIdempotencyKey(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function saveFeedPost(deps, opts) {
|
|
22
|
+
const { signal, state, draft, expected } = opts;
|
|
23
|
+
const stage = (value) => notifyObserver(() => opts.on_stage?.(value));
|
|
24
|
+
const own = async (id) => {
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
const post = (await deps.feed.findOneFeedPost({ id, signal })).value;
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
if (!post)
|
|
29
|
+
throw errResolveFailed({
|
|
30
|
+
resource: "feed_post",
|
|
31
|
+
msg: "Feed post could not be resolved.",
|
|
32
|
+
});
|
|
33
|
+
if (opts.owner_id && post.owner_id !== opts.owner_id)
|
|
34
|
+
throw errValidation({
|
|
35
|
+
field: "owner_id",
|
|
36
|
+
msg: "This post belongs to another owner.",
|
|
37
|
+
});
|
|
38
|
+
return post;
|
|
39
|
+
};
|
|
40
|
+
const stored = async (id) => {
|
|
41
|
+
const out = await deps.storage.downloadObjects({
|
|
42
|
+
col: ColStorageFeed,
|
|
43
|
+
owner_id: id,
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
return out.value ?? [];
|
|
48
|
+
};
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
if (state.original &&
|
|
51
|
+
opts.owner_id &&
|
|
52
|
+
state.original.owner_id !== opts.owner_id)
|
|
53
|
+
throw errValidation({ field: "owner_id", msg: "Account context changed." });
|
|
54
|
+
if (state.rejected)
|
|
55
|
+
return { post: null, upload: state.upload };
|
|
56
|
+
if (draft.files.length)
|
|
57
|
+
validateStorageUploadFiles(draft.files);
|
|
58
|
+
if (draft.files.length &&
|
|
59
|
+
draft.save_to_personal_box &&
|
|
60
|
+
!opts.owner_id?.trim())
|
|
61
|
+
throw errValidation({
|
|
62
|
+
field: "owner_id",
|
|
63
|
+
msg: "A personal copy requires the current user.",
|
|
64
|
+
});
|
|
65
|
+
let input = draft.in;
|
|
66
|
+
if (!state.post_id) {
|
|
67
|
+
if (expected) {
|
|
68
|
+
const current = await own(expected.id);
|
|
69
|
+
checkRevision(current, expected);
|
|
70
|
+
state.original = current;
|
|
71
|
+
state.post_id = current.id;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
input = prepareDraft(draft);
|
|
75
|
+
if (resolvedID(opts.created_id))
|
|
76
|
+
state.post_id = opts.created_id;
|
|
77
|
+
else {
|
|
78
|
+
if (state.create_attempted)
|
|
79
|
+
throw errUnconfirmedMutation();
|
|
80
|
+
const parent_id = resolvedID(input.reply_to_post_id) ??
|
|
81
|
+
resolvedID(input.referenced_post_id);
|
|
82
|
+
if (parent_id) {
|
|
83
|
+
const parent = await deps.read(parent_id, signal);
|
|
84
|
+
const existing = resolvedID(input.reply_to_post_id)
|
|
85
|
+
? parent.self.reply_id
|
|
86
|
+
: parent.self.reference_id;
|
|
87
|
+
if (resolvedID(existing))
|
|
88
|
+
throw errValidation({
|
|
89
|
+
field: "text",
|
|
90
|
+
msg: "A reply or reference already exists.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
stage("create");
|
|
94
|
+
opts.before_request?.();
|
|
95
|
+
state.create_attempted = true;
|
|
96
|
+
let out;
|
|
97
|
+
try {
|
|
98
|
+
out = await deps.feed.insertOneFeedPost({
|
|
99
|
+
in: input,
|
|
100
|
+
signal,
|
|
101
|
+
idempotency_key: opts.idempotency_key,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isClientErr(error) &&
|
|
106
|
+
(error.status === 400 || error.status === 422)) {
|
|
107
|
+
state.create_attempted = false;
|
|
108
|
+
notifyObserver(() => opts.on_rejected?.());
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
// Acknowledgements survive an abort or a failure of the next request.
|
|
113
|
+
state.post_id = requiredOperationID(out, "Created feed post response did not contain an ID.");
|
|
114
|
+
notifyObserver(() => opts.on_created?.(state.post_id));
|
|
115
|
+
if (!draft.files.length)
|
|
116
|
+
state.patched = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const id = state.post_id;
|
|
121
|
+
if (!id)
|
|
122
|
+
throw errResolveFailed({
|
|
123
|
+
resource: "feed_post",
|
|
124
|
+
msg: "Feed post ID is missing.",
|
|
125
|
+
});
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
const rejectUpload = async () => {
|
|
128
|
+
if (!expected) {
|
|
129
|
+
stage("cleanup");
|
|
130
|
+
try {
|
|
131
|
+
await deps.feed.deleteOneFeedPost({ id, signal });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!isClientErr(error) || error.status !== 404)
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
signal.throwIfAborted();
|
|
139
|
+
state.rejected = true;
|
|
140
|
+
if (!expected) {
|
|
141
|
+
deps.changed(id);
|
|
142
|
+
if (resolvedID(state.original?.reply_to_post_id))
|
|
143
|
+
deps.changed(state.original.reply_to_post_id);
|
|
144
|
+
if (resolvedID(state.original?.referenced_post_id))
|
|
145
|
+
deps.changed(state.original.referenced_post_id);
|
|
146
|
+
}
|
|
147
|
+
return { post: null, upload: state.upload };
|
|
148
|
+
};
|
|
149
|
+
if (state.upload?.primary.all_forbidden)
|
|
150
|
+
return rejectUpload();
|
|
151
|
+
state.original ??= await own(id);
|
|
152
|
+
const original = state.original;
|
|
153
|
+
if (opts.owner_id && original.owner_id !== opts.owner_id)
|
|
154
|
+
throw errValidation({ field: "owner_id", msg: "Account context changed." });
|
|
155
|
+
if (!state.patched) {
|
|
156
|
+
if (!state.input) {
|
|
157
|
+
const current = await own(id);
|
|
158
|
+
checkRevision(current, original);
|
|
159
|
+
const base = feedPostInput(original);
|
|
160
|
+
const retained = original.objects.filter((object) => draft.retained_urls.includes(object.public_url));
|
|
161
|
+
let attachments = retained;
|
|
162
|
+
if (draft.retained_urls.some((url) => !original.objects.some((object) => object.public_url === url)))
|
|
163
|
+
throw errValidation({
|
|
164
|
+
field: "objects",
|
|
165
|
+
msg: "An attachment is no longer available.",
|
|
166
|
+
});
|
|
167
|
+
input = prepareDraft({
|
|
168
|
+
...draft,
|
|
169
|
+
in: {
|
|
170
|
+
...base,
|
|
171
|
+
text: draft.in.text,
|
|
172
|
+
visibility: expected ? draft.in.visibility : original.visibility,
|
|
173
|
+
objects: retained,
|
|
174
|
+
},
|
|
175
|
+
}, true);
|
|
176
|
+
const removed = original.objects.filter((object) => !draft.retained_urls.includes(object.public_url));
|
|
177
|
+
if (draft.files.length || removed.length)
|
|
178
|
+
state.baseline ??= await stored(id);
|
|
179
|
+
state.removed ??= (state.baseline ?? []).filter((object) => removed.some((old) => old.public_url === object.public_url));
|
|
180
|
+
if (draft.files.length) {
|
|
181
|
+
stage("upload");
|
|
182
|
+
if (state.upload_attempted && !state.primary)
|
|
183
|
+
throw errUnconfirmedMutation();
|
|
184
|
+
opts.before_request?.();
|
|
185
|
+
state.upload_attempted = true;
|
|
186
|
+
state.upload ??= await deps.storage.uploadObjectsWithPersonalBox({
|
|
187
|
+
col: ColStorageFeed,
|
|
188
|
+
owner_id: id,
|
|
189
|
+
uploader_id: original.owner_id,
|
|
190
|
+
user_id: original.owner_id,
|
|
191
|
+
public_url: true,
|
|
192
|
+
full_quality: draft.full_quality,
|
|
193
|
+
files: draft.files,
|
|
194
|
+
save_to_personal_box: draft.save_to_personal_box,
|
|
195
|
+
signal,
|
|
196
|
+
idempotency_key: state.upload_key,
|
|
197
|
+
personal_box_idempotency_key: state.personal_key,
|
|
198
|
+
primary: state.primary,
|
|
199
|
+
on_primary: (result) => {
|
|
200
|
+
state.primary = result;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
signal.throwIfAborted();
|
|
204
|
+
if (state.upload.primary.all_forbidden) {
|
|
205
|
+
return rejectUpload();
|
|
206
|
+
}
|
|
207
|
+
stage("resolve");
|
|
208
|
+
const objects = await stored(id);
|
|
209
|
+
const uploaded = objects.filter((object) => !state.baseline?.some((before) => before.id === object.id));
|
|
210
|
+
if (uploaded.some((object) => object.owner_id !== id ||
|
|
211
|
+
object.uploader_id !== original.owner_id))
|
|
212
|
+
throw errResolveFailed({
|
|
213
|
+
resource: "feed_object",
|
|
214
|
+
msg: "Upload ownership could not be resolved.",
|
|
215
|
+
});
|
|
216
|
+
// Do not finalize a partial download as a completed, unmoderated upload.
|
|
217
|
+
if (!state.upload.primary.contains_forbidden &&
|
|
218
|
+
uploaded.length !== draft.files.length)
|
|
219
|
+
throw errResolveFailed({
|
|
220
|
+
resource: "feed_object",
|
|
221
|
+
msg: "Uploaded attachments are not yet resolved.",
|
|
222
|
+
});
|
|
223
|
+
attachments = [...retained, ...feedObjects(uploaded)];
|
|
224
|
+
}
|
|
225
|
+
state.input = {
|
|
226
|
+
...input,
|
|
227
|
+
objects: attachments,
|
|
228
|
+
is_uploading: false,
|
|
229
|
+
has_media: attachments.length > 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const current = await own(id);
|
|
233
|
+
// Only an attempted PATCH may be recognized as already committed. An unchanged
|
|
234
|
+
// first Save still runs the real PATCH and the backend's computation.
|
|
235
|
+
if (state.patch_attempted && matchesInput(current, state.input))
|
|
236
|
+
state.patched = true;
|
|
237
|
+
else {
|
|
238
|
+
checkRevision(current, original);
|
|
239
|
+
stage("publish");
|
|
240
|
+
opts.before_request?.();
|
|
241
|
+
state.patch_attempted = true;
|
|
242
|
+
await deps.feed.replaceOneFeedPost({
|
|
243
|
+
id,
|
|
244
|
+
in: state.input,
|
|
245
|
+
signal,
|
|
246
|
+
idempotency_key: state.patch_key,
|
|
247
|
+
});
|
|
248
|
+
state.patched = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
signal.throwIfAborted();
|
|
252
|
+
deps.changed(id);
|
|
253
|
+
if (resolvedID(original.reply_to_post_id))
|
|
254
|
+
deps.changed(original.reply_to_post_id);
|
|
255
|
+
if (resolvedID(original.referenced_post_id))
|
|
256
|
+
deps.changed(original.referenced_post_id);
|
|
257
|
+
if (state.removed?.length) {
|
|
258
|
+
stage("cleanup");
|
|
259
|
+
const current = await own(id);
|
|
260
|
+
for (const object of [...state.removed]) {
|
|
261
|
+
if (current.objects.some((attached) => attached.public_url === object.public_url))
|
|
262
|
+
throw errValidation({
|
|
263
|
+
field: "updated_at",
|
|
264
|
+
msg: "This attachment is in use again.",
|
|
265
|
+
});
|
|
266
|
+
await deps.storage.deleteOwnedObject({
|
|
267
|
+
col: ColStorageFeed,
|
|
268
|
+
id: object.id,
|
|
269
|
+
owner_id: id,
|
|
270
|
+
uploader_id: original.owner_id,
|
|
271
|
+
signal,
|
|
272
|
+
});
|
|
273
|
+
state.removed = state.removed.filter((value) => value.id !== object.id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
stage("resolve");
|
|
277
|
+
return { post: await deps.read(id, signal), upload: state.upload };
|
|
278
|
+
}
|
|
279
|
+
function prepareDraft(draft, editing = false) {
|
|
280
|
+
const input = draft.in;
|
|
281
|
+
validateFeedVisibility(input.visibility);
|
|
282
|
+
const reply = resolvedID(input.reply_to_post_id);
|
|
283
|
+
const reference = resolvedID(input.referenced_post_id);
|
|
284
|
+
if (reply && reference)
|
|
285
|
+
throw errValidation({
|
|
286
|
+
field: "text",
|
|
287
|
+
msg: "A post cannot be both a reply and a reference.",
|
|
288
|
+
});
|
|
289
|
+
const objects = editing ? input.objects : [];
|
|
290
|
+
if (!input.text.text.trim() &&
|
|
291
|
+
!reference &&
|
|
292
|
+
!objects.length &&
|
|
293
|
+
!draft.files.length)
|
|
294
|
+
throw errValidation({
|
|
295
|
+
field: "text",
|
|
296
|
+
msg: "Post text or media is required.",
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
...input,
|
|
300
|
+
text: { text: input.text.text.trim(), file: "", file_url: "", mime: "" },
|
|
301
|
+
objects,
|
|
302
|
+
visibility: !editing && (reply || reference) ? All : input.visibility,
|
|
303
|
+
referenced_post_id: reference ?? ZeroID,
|
|
304
|
+
reply_to_post_id: reply ?? ZeroID,
|
|
305
|
+
has_media: !!(objects.length || draft.files.length),
|
|
306
|
+
is_uploading: !editing && !!draft.files.length,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function checkRevision(current, original) {
|
|
310
|
+
if (current.owner_id !== original.owner_id ||
|
|
311
|
+
current.updated_at !== original.updated_at)
|
|
312
|
+
throw errValidation({
|
|
313
|
+
field: "updated_at",
|
|
314
|
+
msg: "This post has changed. Open it again before editing.",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function matchesInput(post, input) {
|
|
318
|
+
return (post.text.text === input.text.text &&
|
|
319
|
+
post.visibility === input.visibility &&
|
|
320
|
+
post.has_media === input.has_media &&
|
|
321
|
+
post.is_uploading === input.is_uploading &&
|
|
322
|
+
post.reply_to_post_id === input.reply_to_post_id &&
|
|
323
|
+
post.referenced_post_id === input.referenced_post_id &&
|
|
324
|
+
post.objects.length === input.objects.length &&
|
|
325
|
+
post.objects.every((object, index) => object.public_url === input.objects[index].public_url &&
|
|
326
|
+
object.mime_prefix === input.objects[index].mime_prefix));
|
|
327
|
+
}
|
|
328
|
+
export function feedObjects(objects) {
|
|
329
|
+
if (!objects.length)
|
|
330
|
+
throw errResolveFailed({
|
|
331
|
+
resource: "feed_object",
|
|
332
|
+
msg: "Storage did not return any feed objects.",
|
|
333
|
+
});
|
|
334
|
+
return objects.map((object) => {
|
|
335
|
+
const embedding = object.caption;
|
|
336
|
+
if (!object.public_url?.trim() ||
|
|
337
|
+
!object.mime_prefix ||
|
|
338
|
+
!embedding?.plot_embedding?.length ||
|
|
339
|
+
embedding.dim !== embedding.plot_embedding.length ||
|
|
340
|
+
!embedding.plot_embedding.every(Number.isFinite))
|
|
341
|
+
throw errResolveFailed({
|
|
342
|
+
resource: "feed_object",
|
|
343
|
+
msg: "Storage did not return a public URL and complete caption embedding.",
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
public_url: object.public_url,
|
|
347
|
+
mime_prefix: object.mime_prefix,
|
|
348
|
+
embedding: {
|
|
349
|
+
...embedding,
|
|
350
|
+
plot_embedding: [...embedding.plot_embedding],
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type FeedPost, type InFeedPost } from "@fabriktor/schema";
|
|
2
|
+
export declare function validateFeedVisibility(value: number): void;
|
|
3
|
+
export declare function feedPostInput(post: FeedPost): InFeedPost;
|
|
4
|
+
export declare function prepareFeedTextPost(input: InFeedPost): InFeedPost;
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { All, Contacts, Private, ZeroID, } from "@fabriktor/schema";
|
|
7
|
+
import { errValidation } from "../core/err.js";
|
|
8
|
+
import { resolvedID } from "../core/id.js";
|
|
9
|
+
export function validateFeedVisibility(value) {
|
|
10
|
+
if (![All, Contacts, Private].includes(value))
|
|
11
|
+
throw errValidation({
|
|
12
|
+
field: "visibility",
|
|
13
|
+
msg: "Select a valid visibility.",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function feedPostInput(post) {
|
|
17
|
+
return {
|
|
18
|
+
text: { text: post.text.text, file: "", file_url: "", mime: "" },
|
|
19
|
+
objects: post.objects.map((object) => ({
|
|
20
|
+
public_url: object.public_url,
|
|
21
|
+
mime_prefix: object.mime_prefix,
|
|
22
|
+
embedding: {
|
|
23
|
+
...object.embedding,
|
|
24
|
+
plot_embedding: [...object.embedding.plot_embedding],
|
|
25
|
+
},
|
|
26
|
+
})),
|
|
27
|
+
is_uploading: post.is_uploading,
|
|
28
|
+
has_media: post.has_media,
|
|
29
|
+
visibility: post.visibility,
|
|
30
|
+
referenced_post_id: post.referenced_post_id,
|
|
31
|
+
reply_to_post_id: post.reply_to_post_id,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Text publishing uses generated inputs; media creation has its own storage workflow.
|
|
35
|
+
export function prepareFeedTextPost(input) {
|
|
36
|
+
validateFeedVisibility(input.visibility);
|
|
37
|
+
const reply = resolvedID(input.reply_to_post_id);
|
|
38
|
+
const reference = resolvedID(input.referenced_post_id);
|
|
39
|
+
if (reply && reference)
|
|
40
|
+
throw errValidation({
|
|
41
|
+
field: "text",
|
|
42
|
+
msg: "A post cannot be both a reply and a reference.",
|
|
43
|
+
});
|
|
44
|
+
if (input.objects.length || input.has_media || input.is_uploading)
|
|
45
|
+
throw errValidation({
|
|
46
|
+
field: "text",
|
|
47
|
+
msg: "Use the media workflow for media posts.",
|
|
48
|
+
});
|
|
49
|
+
if (!input.text.text.trim() && !reference)
|
|
50
|
+
throw errValidation({ field: "text", msg: "Post text is required." });
|
|
51
|
+
return {
|
|
52
|
+
...input,
|
|
53
|
+
objects: [],
|
|
54
|
+
text: { text: input.text.text.trim(), file: "", file_url: "", mime: "" },
|
|
55
|
+
visibility: reply || reference ? All : input.visibility,
|
|
56
|
+
referenced_post_id: reference ?? ZeroID,
|
|
57
|
+
reply_to_post_id: reply ?? ZeroID,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/src/fx.js
CHANGED
|
@@ -108,12 +108,14 @@ export class FX {
|
|
|
108
108
|
storage: opts.storage,
|
|
109
109
|
});
|
|
110
110
|
this.feed = newFeedFX({
|
|
111
|
+
events: this.events,
|
|
111
112
|
feed: opts.feed,
|
|
112
113
|
storage_fx: this.storage,
|
|
113
114
|
keep_alive_interval_ms: opts.feed_keep_alive_interval_ms,
|
|
114
115
|
});
|
|
115
116
|
this.marketplace = newMarketplaceFX({
|
|
116
117
|
marketplace: opts.marketplace,
|
|
118
|
+
events: this.events,
|
|
117
119
|
keep_alive_interval_ms: opts.marketplace_keep_alive_interval_ms,
|
|
118
120
|
});
|
|
119
121
|
this.timesheets = newTimesheetsFX({
|
package/dist/src/index.d.ts
CHANGED
|
@@ -25,10 +25,13 @@ export * from "./core/search-controller.js";
|
|
|
25
25
|
export * from "./core/search.js";
|
|
26
26
|
export * from "./core/server-clock.js";
|
|
27
27
|
export * from "./feed/feed.js";
|
|
28
|
+
export * from "./feed/media.js";
|
|
29
|
+
export * from "./feed/validation.js";
|
|
28
30
|
export * from "./fulfillments/fulfillments.js";
|
|
29
31
|
export * from "./fx.js";
|
|
30
32
|
export * from "./jobs/jobs.js";
|
|
31
33
|
export * from "./marketplace/marketplace.js";
|
|
34
|
+
export * from "./marketplace/validation.js";
|
|
32
35
|
export * from "./memos/memos.js";
|
|
33
36
|
export * from "./notifications/notifications.js";
|
|
34
37
|
export * from "./payments/payments.js";
|
package/dist/src/index.js
CHANGED
|
@@ -25,10 +25,13 @@ export * from "./core/search-controller.js";
|
|
|
25
25
|
export * from "./core/search.js";
|
|
26
26
|
export * from "./core/server-clock.js";
|
|
27
27
|
export * from "./feed/feed.js";
|
|
28
|
+
export * from "./feed/media.js";
|
|
29
|
+
export * from "./feed/validation.js";
|
|
28
30
|
export * from "./fulfillments/fulfillments.js";
|
|
29
31
|
export * from "./fx.js";
|
|
30
32
|
export * from "./jobs/jobs.js";
|
|
31
33
|
export * from "./marketplace/marketplace.js";
|
|
34
|
+
export * from "./marketplace/validation.js";
|
|
32
35
|
export * from "./memos/memos.js";
|
|
33
36
|
export * from "./notifications/notifications.js";
|
|
34
37
|
export * from "./payments/payments.js";
|
|
@@ -1,17 +1,44 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MarketplaceOperator, type OperationResultOf } from "@fabriktor/client";
|
|
2
2
|
import { type MarketplaceFavorite, type MarketplacePost, type RPMarketplacePull } from "@fabriktor/schema";
|
|
3
3
|
import { type PullSession, type PullSessionErrorHandler } from "../core/pull.js";
|
|
4
4
|
import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
|
|
5
|
+
import type { MultiCreateCheckpoint } from "../core/multi-create.js";
|
|
6
|
+
import { EventBus } from "../core/events.js";
|
|
7
|
+
export * from "./validation.js";
|
|
8
|
+
export declare const MarketplaceEvents: {
|
|
9
|
+
Changed: import("../index.js").EventKey<{
|
|
10
|
+
owner_id: string;
|
|
11
|
+
resource: "posts" | "favorites";
|
|
12
|
+
post_id: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
5
15
|
type MarketplacePostInsertOptions = Parameters<MarketplaceOperator["insertOneMarketplacePost"]>[0];
|
|
6
16
|
type MarketplacePostReplaceOptions = Parameters<MarketplaceOperator["replaceOneMarketplacePost"]>[0];
|
|
7
17
|
type MarketplaceFavoriteInsertOptions = Parameters<MarketplaceOperator["insertOneMarketplaceFavorite"]>[0];
|
|
8
18
|
type MarketplaceFavoriteReplaceOptions = Parameters<MarketplaceOperator["replaceOneMarketplaceFavorite"]>[0];
|
|
9
19
|
type MarketplacePullOptions = Parameters<MarketplaceOperator["pull"]>[0];
|
|
10
|
-
export type CreateMarketplacePostOptions = MarketplacePostInsertOptions;
|
|
11
|
-
export type ReplaceMarketplacePostOptions = MarketplacePostReplaceOptions
|
|
20
|
+
export type CreateMarketplacePostOptions = MarketplacePostInsertOptions & MultiCreateCheckpoint;
|
|
21
|
+
export type ReplaceMarketplacePostOptions = MarketplacePostReplaceOptions & MultiCreateCheckpoint & {
|
|
22
|
+
expected?: Pick<MarketplacePost, "owner_id" | "updated_at">;
|
|
23
|
+
};
|
|
12
24
|
export type CreateMarketplaceFavoriteOptions = MarketplaceFavoriteInsertOptions;
|
|
13
25
|
export type ReplaceMarketplaceFavoriteOptions = MarketplaceFavoriteReplaceOptions;
|
|
14
|
-
export type SearchMarketplacePostsOptions = OwnerSearchOptions
|
|
26
|
+
export type SearchMarketplacePostsOptions = OwnerSearchOptions & {
|
|
27
|
+
post_type?: MarketplacePost["post_type"];
|
|
28
|
+
user_type?: MarketplacePost["user_type"];
|
|
29
|
+
};
|
|
30
|
+
export type SavedMarketplacePost = MarketplaceFavorite & {
|
|
31
|
+
post: MarketplacePost | null;
|
|
32
|
+
};
|
|
33
|
+
export type MarketplaceFavoriteOptions = {
|
|
34
|
+
owner_id: string;
|
|
35
|
+
post_id?: string;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
export type SetMarketplaceFavoriteOptions = MarketplaceFavoriteOptions & {
|
|
39
|
+
post_id: string;
|
|
40
|
+
saved: boolean;
|
|
41
|
+
};
|
|
15
42
|
export type SearchMarketplacePostsPage = SearchPage<MarketplacePost>;
|
|
16
43
|
export type MarketplacePullContext = Pick<MarketplacePullOptions, "pull_type" | "entity_id" | "research" | "research_user_type">;
|
|
17
44
|
export type MarketplacePullResult = OperationResultOf<RPMarketplacePull>;
|
|
@@ -19,19 +46,34 @@ export type MarketplaceResearchUserType = MarketplacePullOptions["research_user_
|
|
|
19
46
|
export type MarketplaceSession = PullSession<MarketplacePullResult>;
|
|
20
47
|
export type MarketplaceSessionErrorHandler = PullSessionErrorHandler;
|
|
21
48
|
export type MarketplaceFXOptions = {
|
|
49
|
+
events?: EventBus;
|
|
22
50
|
marketplace: MarketplaceOperator;
|
|
23
51
|
keep_alive_interval_ms: number;
|
|
24
52
|
};
|
|
25
53
|
export type StartMarketplaceSessionOptions = MarketplacePullContext & {
|
|
54
|
+
signal?: AbortSignal;
|
|
26
55
|
on_error?: MarketplaceSessionErrorHandler;
|
|
27
56
|
};
|
|
28
57
|
export type StartMarketplaceWelcomeSessionOptions = {
|
|
58
|
+
signal?: AbortSignal;
|
|
29
59
|
country: string;
|
|
30
60
|
state?: string;
|
|
31
61
|
research_user_type: MarketplaceResearchUserType;
|
|
32
62
|
on_error?: MarketplaceSessionErrorHandler;
|
|
33
63
|
};
|
|
34
64
|
export interface MarketplaceFXOperator {
|
|
65
|
+
reset(): void;
|
|
66
|
+
readMarketplacePost(opts: {
|
|
67
|
+
id: string;
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
}): Promise<MarketplacePost | null>;
|
|
70
|
+
listMarketplaceFavorites(opts: MarketplaceFavoriteOptions): Promise<MarketplaceFavorite[]>;
|
|
71
|
+
listSavedMarketplacePosts(opts: MarketplaceFavoriteOptions): Promise<SavedMarketplacePost[]>;
|
|
72
|
+
setMarketplaceFavorite(opts: SetMarketplaceFavoriteOptions): Promise<MarketplaceFavorite | null>;
|
|
73
|
+
deleteMarketplacePost(opts: {
|
|
74
|
+
record: MarketplacePost;
|
|
75
|
+
owner_id: string;
|
|
76
|
+
}): Promise<void>;
|
|
35
77
|
createMarketplacePost(opts: CreateMarketplacePostOptions): Promise<MarketplacePost>;
|
|
36
78
|
replaceMarketplacePost(opts: ReplaceMarketplacePostOptions): Promise<MarketplacePost>;
|
|
37
79
|
searchMarketplacePosts(opts: SearchMarketplacePostsOptions): Promise<SearchMarketplacePostsPage>;
|
|
@@ -41,9 +83,13 @@ export interface MarketplaceFXOperator {
|
|
|
41
83
|
startWelcomeSession(opts: StartMarketplaceWelcomeSessionOptions): Promise<MarketplaceSession>;
|
|
42
84
|
}
|
|
43
85
|
export declare class MarketplaceFX implements MarketplaceFXOperator {
|
|
86
|
+
private readonly sessions;
|
|
87
|
+
private readonly events;
|
|
88
|
+
private mutating;
|
|
44
89
|
private marketplace;
|
|
45
90
|
private keep_alive_interval_ms;
|
|
46
91
|
constructor(opts: MarketplaceFXOptions);
|
|
92
|
+
reset(): void;
|
|
47
93
|
createMarketplacePost(opts: CreateMarketplacePostOptions): Promise<MarketplacePost>;
|
|
48
94
|
replaceMarketplacePost(opts: ReplaceMarketplacePostOptions): Promise<MarketplacePost>;
|
|
49
95
|
searchMarketplacePosts(opts: SearchMarketplacePostsOptions): Promise<SearchMarketplacePostsPage>;
|
|
@@ -51,8 +97,21 @@ export declare class MarketplaceFX implements MarketplaceFXOperator {
|
|
|
51
97
|
replaceMarketplaceFavorite(opts: ReplaceMarketplaceFavoriteOptions): Promise<MarketplaceFavorite>;
|
|
52
98
|
startSession(opts: StartMarketplaceSessionOptions): Promise<MarketplaceSession>;
|
|
53
99
|
startWelcomeSession(opts: StartMarketplaceWelcomeSessionOptions): Promise<MarketplaceSession>;
|
|
100
|
+
readMarketplacePost(opts: {
|
|
101
|
+
id: string;
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}): Promise<MarketplacePost | null>;
|
|
104
|
+
listMarketplaceFavorites(opts: MarketplaceFavoriteOptions): Promise<MarketplaceFavorite[]>;
|
|
105
|
+
listSavedMarketplacePosts(opts: MarketplaceFavoriteOptions): Promise<SavedMarketplacePost[]>;
|
|
106
|
+
setMarketplaceFavorite(opts: SetMarketplaceFavoriteOptions): Promise<MarketplaceFavorite | null>;
|
|
107
|
+
deleteMarketplacePost(opts: {
|
|
108
|
+
record: MarketplacePost;
|
|
109
|
+
owner_id: string;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
private mutate;
|
|
112
|
+
private requireID;
|
|
113
|
+
private changed;
|
|
54
114
|
private findMarketplacePost;
|
|
55
115
|
private findMarketplaceFavorite;
|
|
56
116
|
}
|
|
57
117
|
export declare function newMarketplaceFX(opts: MarketplaceFXOptions): MarketplaceFX;
|
|
58
|
-
export {};
|