@headless-lms/core 0.2.0 → 0.3.0
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/assets/events.d.ts +39 -21
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +3 -2
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +45 -15
- package/dist/assets/service.d.ts +39 -7
- package/dist/assets/service.js +291 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +168 -43
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +2 -1
- package/dist/automations/catalog.js +229 -13
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +112 -0
- package/dist/automations/community-mail.js +90 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/community-notifications.d.ts +16 -0
- package/dist/automations/community-notifications.js +84 -0
- package/dist/automations/community-notifications.js.map +1 -0
- package/dist/automations/events.d.ts +28 -0
- package/dist/automations/index.d.ts +4 -2
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +5 -1
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/seeds.d.ts +17 -0
- package/dist/automations/seeds.js +235 -0
- package/dist/automations/seeds.js.map +1 -0
- package/dist/automations/service.d.ts +22 -3
- package/dist/automations/service.js +306 -43
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +631 -19
- package/dist/content/events.js +44 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +4 -4
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +71 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +502 -23
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +104 -0
- package/dist/discussion/chats.js +798 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/events.d.ts +619 -56
- package/dist/discussion/events.js +106 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -3
- package/dist/discussion/index.js +4 -1
- package/dist/discussion/ports.d.ts +288 -27
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +103 -0
- package/dist/discussion/posts.js +723 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +32 -10
- package/dist/discussion/service.js +293 -78
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +124 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +54 -3
- package/dist/entitlements/service.js +472 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/index.d.ts +6 -4
- package/dist/identity/index.js +3 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notifications.d.ts +27 -0
- package/dist/identity/notifications.js +101 -0
- package/dist/identity/notifications.js.map +1 -0
- package/dist/identity/ports.d.ts +73 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +74 -32
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +7 -6
- package/dist/organizations/index.js +4 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +14 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +20 -11
- package/dist/organizations/service.js +157 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +124 -0
- package/dist/reporting/member/ports.d.ts +45 -0
- package/dist/reporting/member/service.d.ts +73 -0
- package/dist/reporting/member/service.js +286 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +7 -1
- package/dist/shared/errors.js +10 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +11 -0
- package/dist/shared/id.js +12 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +4 -1
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +10 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +121 -0
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -9
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +18 -1
- package/dist/types/schemas/automations.js +15 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +335 -3
- package/dist/types/schemas/content.js +139 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +370 -3
- package/dist/types/schemas/discussion.js +199 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +111 -1
- package/dist/types/schemas/identity.js +55 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -8
- package/dist/types/schemas/index.js +8 -8
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/ports.js +0 -1
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
package/dist/assets/service.js
CHANGED
|
@@ -1,27 +1,59 @@
|
|
|
1
|
-
import { noopLogger } from "../shared/logger.js";
|
|
2
|
-
import { genId } from "../shared/id.js";
|
|
3
1
|
import { assetEvents } from "./events.js";
|
|
2
|
+
import { genId } from "../shared/id.js";
|
|
3
|
+
import { ConflictError, ForbiddenError, InvalidInputError } from "../shared/errors.js";
|
|
4
|
+
import { noopLogger } from "../shared/logger.js";
|
|
4
5
|
//#region src/assets/service.ts
|
|
6
|
+
const UNATTACHED_UPLOAD_GRACE_MS = 3600 * 1e3;
|
|
7
|
+
const MEMBER_IMAGE_TYPE = /^image\/(png|jpeg|gif|webp|avif)$/i;
|
|
8
|
+
const MEMBER_IMAGE_REFUSAL = "A member upload must be a PNG, JPEG, GIF, WebP or AVIF image";
|
|
9
|
+
function isMemberImageType(contentType) {
|
|
10
|
+
return MEMBER_IMAGE_TYPE.test(contentType.trim());
|
|
11
|
+
}
|
|
12
|
+
function kindOf(contentType) {
|
|
13
|
+
const type = contentType.trim().toLowerCase();
|
|
14
|
+
if (type.startsWith("image/")) return "image";
|
|
15
|
+
if (type.startsWith("video/")) return "video";
|
|
16
|
+
if (type.startsWith("audio/")) return "audio";
|
|
17
|
+
return "document";
|
|
18
|
+
}
|
|
5
19
|
function orgPrefix(orgId) {
|
|
6
20
|
return `org/${orgId}/`;
|
|
7
21
|
}
|
|
8
22
|
function sanitizeFilename(filename) {
|
|
9
23
|
return (filename.split(/[\\/]/).pop() ?? "file").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120) || "file";
|
|
10
24
|
}
|
|
25
|
+
function storedKey(orgId, folder, id, filename) {
|
|
26
|
+
return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;
|
|
27
|
+
}
|
|
28
|
+
function finalMemberKey(orgId, asset) {
|
|
29
|
+
return `${orgPrefix(orgId)}image/${asset.id}/${sanitizeFilename(asset.filename)}`;
|
|
30
|
+
}
|
|
31
|
+
function graceCutoff(now) {
|
|
32
|
+
return /* @__PURE__ */ new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);
|
|
33
|
+
}
|
|
11
34
|
var AssetsServiceImpl = class {
|
|
12
35
|
storage;
|
|
13
36
|
repo;
|
|
14
37
|
uow;
|
|
38
|
+
memberImageMaxBytes;
|
|
39
|
+
downloadLinkExpirySeconds;
|
|
40
|
+
fileUrls;
|
|
15
41
|
logger;
|
|
16
42
|
constructor(params) {
|
|
43
|
+
if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) throw new Error(`invalid memberImageMaxBytes "${params.memberImageMaxBytes}" (expected a positive integer)`);
|
|
44
|
+
if (!Number.isSafeInteger(params.downloadLinkExpirySeconds) || params.downloadLinkExpirySeconds <= 0) throw new Error(`invalid downloadLinkExpirySeconds "${params.downloadLinkExpirySeconds}" (expected a positive integer)`);
|
|
17
45
|
this.storage = params.storage;
|
|
18
46
|
this.repo = params.repo;
|
|
19
47
|
this.uow = params.uow;
|
|
48
|
+
this.memberImageMaxBytes = params.memberImageMaxBytes;
|
|
49
|
+
this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;
|
|
50
|
+
this.fileUrls = params.fileUrls;
|
|
20
51
|
this.logger = params.logger ?? noopLogger;
|
|
21
52
|
}
|
|
22
53
|
async requestUpload(orgId, input) {
|
|
23
54
|
const id = genId("asset");
|
|
24
|
-
const
|
|
55
|
+
const kind = kindOf(input.contentType);
|
|
56
|
+
const key = storedKey(orgId, input.source === "member" ? "uploads" : kind, id, input.filename);
|
|
25
57
|
const presigned = await this.storage.presignUpload({
|
|
26
58
|
key,
|
|
27
59
|
contentType: input.contentType
|
|
@@ -32,7 +64,8 @@ var AssetsServiceImpl = class {
|
|
|
32
64
|
orgId,
|
|
33
65
|
id,
|
|
34
66
|
key,
|
|
35
|
-
kind
|
|
67
|
+
kind,
|
|
68
|
+
source: input.source,
|
|
36
69
|
filename: input.filename,
|
|
37
70
|
contentType: input.contentType,
|
|
38
71
|
size: 0,
|
|
@@ -50,83 +83,303 @@ var AssetsServiceImpl = class {
|
|
|
50
83
|
this.logger.info("asset upload requested", {
|
|
51
84
|
orgId,
|
|
52
85
|
assetId: id,
|
|
53
|
-
kind
|
|
86
|
+
kind,
|
|
87
|
+
source: input.source
|
|
54
88
|
});
|
|
55
89
|
return {
|
|
56
|
-
asset
|
|
90
|
+
asset: {
|
|
91
|
+
...asset,
|
|
92
|
+
url: null,
|
|
93
|
+
downloadUrl: null
|
|
94
|
+
},
|
|
57
95
|
uploadUrl: presigned.url,
|
|
58
96
|
method: "PUT",
|
|
59
97
|
expiresInSeconds: presigned.expiresInSeconds,
|
|
60
98
|
headers: presigned.headers
|
|
61
99
|
};
|
|
62
100
|
}
|
|
101
|
+
async requestMemberUpload(orgId, orgUserId, input) {
|
|
102
|
+
if (!isMemberImageType(input.contentType)) throw new InvalidInputError(MEMBER_IMAGE_REFUSAL);
|
|
103
|
+
return this.requestUpload(orgId, {
|
|
104
|
+
filename: input.filename,
|
|
105
|
+
contentType: input.contentType,
|
|
106
|
+
source: "member",
|
|
107
|
+
uploadedBy: orgUserId
|
|
108
|
+
});
|
|
109
|
+
}
|
|
63
110
|
async confirm(orgId, id) {
|
|
64
111
|
const asset = await this.repo.findById(orgId, id);
|
|
65
112
|
if (!asset) return null;
|
|
113
|
+
if (asset.source === "member") throw new ForbiddenError("Only the uploader can finalize a member upload");
|
|
114
|
+
const confirmed = await this.finalize(orgId, asset);
|
|
115
|
+
return confirmed ? this.view(confirmed) : null;
|
|
116
|
+
}
|
|
117
|
+
async confirmOwn(orgId, id, orgUserId) {
|
|
118
|
+
const asset = await this.repo.findById(orgId, id);
|
|
119
|
+
if (!asset || asset.source !== "member" || asset.uploadedBy !== orgUserId) {
|
|
120
|
+
this.logger.debug("upload confirm refused", {
|
|
121
|
+
orgId,
|
|
122
|
+
assetId: id,
|
|
123
|
+
orgUserId
|
|
124
|
+
});
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const confirmed = await this.finalize(orgId, asset);
|
|
128
|
+
if (!confirmed) return null;
|
|
129
|
+
return confirmed.status === "ready" ? {
|
|
130
|
+
...confirmed,
|
|
131
|
+
url: this.fileUrls.memberUpload(confirmed.id),
|
|
132
|
+
downloadUrl: null
|
|
133
|
+
} : {
|
|
134
|
+
...confirmed,
|
|
135
|
+
url: null,
|
|
136
|
+
downloadUrl: null
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async ownUploadFile(orgId, assetId, orgUserId) {
|
|
140
|
+
if (!await this.ownsImage(orgId, assetId, orgUserId)) {
|
|
141
|
+
this.logger.warn("upload file not served — not the uploader", {
|
|
142
|
+
orgId,
|
|
143
|
+
assetId,
|
|
144
|
+
orgUserId
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return this.signFile(orgId, assetId);
|
|
149
|
+
}
|
|
150
|
+
async ownsImage(orgId, assetId, orgUserId) {
|
|
151
|
+
const asset = await this.repo.findById(orgId, assetId);
|
|
152
|
+
return asset !== null && asset.source === "member" && asset.kind === "image" && asset.status === "ready" && asset.uploadedBy === orgUserId;
|
|
153
|
+
}
|
|
154
|
+
async libraryAssetsAmong(orgId, assetIds) {
|
|
155
|
+
if (assetIds.length === 0) return [];
|
|
156
|
+
const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);
|
|
157
|
+
const library = new Set(rows.filter((a) => a.source === "library" && a.status === "ready").map((a) => a.id));
|
|
158
|
+
return assetIds.filter((id) => library.has(id));
|
|
159
|
+
}
|
|
160
|
+
async list(orgId, query) {
|
|
161
|
+
const page = await this.repo.list(orgId, {
|
|
162
|
+
...query,
|
|
163
|
+
source: query.source ?? "library"
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
...page,
|
|
167
|
+
rows: await Promise.all(page.rows.map((asset) => this.view(asset)))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async get(orgId, id) {
|
|
171
|
+
const asset = await this.repo.findById(orgId, id);
|
|
172
|
+
return asset ? this.view(asset) : null;
|
|
173
|
+
}
|
|
174
|
+
async readyFiles(orgId, assetIds) {
|
|
175
|
+
const ready = await this.readyAmong(orgId, assetIds);
|
|
176
|
+
return Object.fromEntries(ready.map((asset) => [asset.id, {
|
|
177
|
+
filename: asset.filename,
|
|
178
|
+
contentType: asset.contentType,
|
|
179
|
+
size: asset.size
|
|
180
|
+
}]));
|
|
181
|
+
}
|
|
182
|
+
async signFile(orgId, assetId, opts = {}) {
|
|
183
|
+
const [asset] = await this.readyAmong(orgId, [assetId]);
|
|
184
|
+
if (!asset) {
|
|
185
|
+
this.logger.warn("file not served — no ready asset", {
|
|
186
|
+
orgId,
|
|
187
|
+
assetId
|
|
188
|
+
});
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return opts.saveAs === void 0 ? this.storage.presignDownload({ key: asset.key }) : this.storage.presignDownload({
|
|
192
|
+
key: asset.key,
|
|
193
|
+
downloadFilename: opts.saveAs,
|
|
194
|
+
expiresInSeconds: this.downloadLinkExpirySeconds
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async libraryFile(orgId, assetId, opts) {
|
|
198
|
+
const asset = await this.repo.findById(orgId, assetId);
|
|
199
|
+
if (!asset || asset.source !== "library") {
|
|
200
|
+
this.logger.warn("library file not served — not a library asset", {
|
|
201
|
+
orgId,
|
|
202
|
+
assetId
|
|
203
|
+
});
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});
|
|
207
|
+
}
|
|
208
|
+
async remove(orgId, id) {
|
|
209
|
+
const asset = await this.repo.findById(orgId, id);
|
|
210
|
+
if (!asset) return false;
|
|
211
|
+
const removed = await this.discard(orgId, asset);
|
|
212
|
+
if (removed) this.logger.info("asset removed", {
|
|
213
|
+
orgId,
|
|
214
|
+
assetId: id
|
|
215
|
+
});
|
|
216
|
+
return removed;
|
|
217
|
+
}
|
|
218
|
+
async removeOwn(orgId, id, orgUserId) {
|
|
219
|
+
const asset = await this.repo.findById(orgId, id);
|
|
220
|
+
if (!asset || asset.source !== "member" || asset.uploadedBy !== orgUserId) return false;
|
|
221
|
+
return this.remove(orgId, id);
|
|
222
|
+
}
|
|
223
|
+
async hasUnattached(orgId, now) {
|
|
224
|
+
if (await this.repo.hasUnreferencedMemberUploads(orgId, graceCutoff(now))) return true;
|
|
225
|
+
return (await this.strayUploads(orgId, now)).length > 0;
|
|
226
|
+
}
|
|
227
|
+
async removeUnattached(orgId, now) {
|
|
228
|
+
const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));
|
|
229
|
+
let removed = 0;
|
|
230
|
+
let failed = 0;
|
|
231
|
+
for (const asset of stale) try {
|
|
232
|
+
if (await this.discard(orgId, asset)) removed += 1;
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!(err instanceof ConflictError)) {
|
|
235
|
+
failed += 1;
|
|
236
|
+
this.logger.error("unattached upload not removed", {
|
|
237
|
+
orgId,
|
|
238
|
+
assetId: asset.id,
|
|
239
|
+
err
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let stray = 0;
|
|
244
|
+
for (const key of await this.strayUploads(orgId, now)) try {
|
|
245
|
+
await this.storage.remove(key);
|
|
246
|
+
stray += 1;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
failed += 1;
|
|
249
|
+
this.logger.warn("stray upload not removed", {
|
|
250
|
+
orgId,
|
|
251
|
+
err
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (removed > 0 || stray > 0 || failed > 0) this.logger.info("unattached uploads removed", {
|
|
255
|
+
orgId,
|
|
256
|
+
count: removed,
|
|
257
|
+
stray,
|
|
258
|
+
failed
|
|
259
|
+
});
|
|
260
|
+
return removed + stray;
|
|
261
|
+
}
|
|
262
|
+
async strayUploads(orgId, now) {
|
|
263
|
+
const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1e3;
|
|
264
|
+
const old = (await this.storage.list(`${orgPrefix(orgId)}uploads/`)).filter((o) => o.lastModified !== void 0 && Date.parse(o.lastModified) < cutoff).map((o) => o.key);
|
|
265
|
+
if (old.length === 0) return [];
|
|
266
|
+
const held = new Set(await this.repo.heldKeys(orgId, old));
|
|
267
|
+
return old.filter((key) => !held.has(key));
|
|
268
|
+
}
|
|
269
|
+
async finalize(orgId, asset) {
|
|
270
|
+
if (asset.status !== "pending") return asset;
|
|
271
|
+
if (asset.source === "member") return this.finalizeMember(orgId, asset);
|
|
66
272
|
const stat = await this.storage.stat(asset.key);
|
|
67
273
|
if (!stat) {
|
|
68
274
|
this.logger.debug("asset not yet uploaded", {
|
|
69
275
|
orgId,
|
|
70
|
-
assetId: id
|
|
276
|
+
assetId: asset.id
|
|
71
277
|
});
|
|
72
278
|
return asset;
|
|
73
279
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
280
|
+
const contentType = stat.contentType ?? asset.contentType;
|
|
281
|
+
return await this.markReady(orgId, asset, {
|
|
282
|
+
key: asset.key,
|
|
283
|
+
kind: kindOf(contentType),
|
|
284
|
+
size: stat.size,
|
|
285
|
+
contentType
|
|
286
|
+
}) ?? this.repo.findById(orgId, asset.id);
|
|
287
|
+
}
|
|
288
|
+
async finalizeMember(orgId, asset) {
|
|
289
|
+
if (!await this.storage.stat(asset.key)) {
|
|
290
|
+
this.logger.debug("asset not yet uploaded", {
|
|
291
|
+
orgId,
|
|
292
|
+
assetId: asset.id
|
|
79
293
|
});
|
|
294
|
+
return asset;
|
|
295
|
+
}
|
|
296
|
+
const key = finalMemberKey(orgId, asset);
|
|
297
|
+
await this.storage.copy({
|
|
298
|
+
from: asset.key,
|
|
299
|
+
to: key
|
|
300
|
+
});
|
|
301
|
+
const stat = await this.storage.stat(key);
|
|
302
|
+
if (!stat) throw new Error("copied upload not found");
|
|
303
|
+
const contentType = asset.contentType;
|
|
304
|
+
const kind = kindOf(contentType);
|
|
305
|
+
const reason = !isMemberImageType(contentType) ? MEMBER_IMAGE_REFUSAL : stat.contentType !== void 0 && stat.contentType !== contentType ? "The uploaded file is not the declared type" : stat.size > this.memberImageMaxBytes ? "This image is too large" : null;
|
|
306
|
+
if (reason) {
|
|
307
|
+
await this.removeObject(orgId, asset.id, key);
|
|
308
|
+
await this.discard(orgId, asset);
|
|
309
|
+
this.logger.info("member upload refused", {
|
|
310
|
+
orgId,
|
|
311
|
+
assetId: asset.id,
|
|
312
|
+
reason
|
|
313
|
+
});
|
|
314
|
+
throw new InvalidInputError(reason);
|
|
315
|
+
}
|
|
316
|
+
const ready = await this.markReady(orgId, asset, {
|
|
317
|
+
key,
|
|
318
|
+
kind,
|
|
319
|
+
size: stat.size,
|
|
320
|
+
contentType
|
|
321
|
+
});
|
|
322
|
+
if (!ready) return this.repo.findById(orgId, asset.id);
|
|
323
|
+
await this.removeObject(orgId, asset.id, asset.key);
|
|
324
|
+
return ready;
|
|
325
|
+
}
|
|
326
|
+
async markReady(orgId, asset, patch) {
|
|
327
|
+
const updated = await this.uow.run(async ({ assets, outbox }) => {
|
|
328
|
+
const updated = await assets.markReady(orgId, asset.id, patch);
|
|
80
329
|
if (updated) await outbox.append([assetEvents.assetReady.make({
|
|
81
330
|
orgId,
|
|
82
331
|
data: updated
|
|
83
332
|
})]);
|
|
84
333
|
return updated;
|
|
85
334
|
});
|
|
86
|
-
this.logger.info("asset confirmed", {
|
|
335
|
+
if (updated) this.logger.info("asset confirmed", {
|
|
87
336
|
orgId,
|
|
88
|
-
assetId: id
|
|
337
|
+
assetId: asset.id
|
|
89
338
|
});
|
|
90
339
|
return updated;
|
|
91
340
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return this.repo.findById(orgId, id);
|
|
341
|
+
async readyAmong(orgId, assetIds) {
|
|
342
|
+
const ids = [...new Set(assetIds)];
|
|
343
|
+
if (ids.length === 0) return [];
|
|
344
|
+
return (await this.repo.findByIds(orgId, ids)).filter((asset) => asset.orgId === orgId && asset.status === "ready");
|
|
97
345
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
346
|
+
view(asset) {
|
|
347
|
+
if (asset.source !== "library" || asset.status !== "ready") return {
|
|
348
|
+
...asset,
|
|
349
|
+
url: null,
|
|
350
|
+
downloadUrl: null
|
|
351
|
+
};
|
|
101
352
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
...expiresInSeconds === void 0 ? {} : { expiresInSeconds }
|
|
106
|
-
}),
|
|
107
|
-
asset
|
|
353
|
+
...asset,
|
|
354
|
+
url: this.fileUrls.libraryFile(asset.id),
|
|
355
|
+
downloadUrl: this.fileUrls.libraryDownload(asset.id)
|
|
108
356
|
};
|
|
109
357
|
}
|
|
110
|
-
async
|
|
111
|
-
const asset = await this.repo.findById(orgId, id);
|
|
112
|
-
if (!asset) return false;
|
|
113
|
-
await this.storage.remove(asset.key);
|
|
358
|
+
async discard(orgId, asset) {
|
|
114
359
|
const deleted = await this.uow.run(async ({ assets, outbox }) => {
|
|
115
|
-
const deleted = await assets.delete(id);
|
|
360
|
+
const deleted = await assets.delete(orgId, asset.id);
|
|
116
361
|
if (deleted) await outbox.append([assetEvents.assetDeleted.make({
|
|
117
362
|
orgId,
|
|
118
363
|
data: asset
|
|
119
364
|
})]);
|
|
120
365
|
return deleted;
|
|
121
366
|
});
|
|
122
|
-
this.
|
|
123
|
-
orgId,
|
|
124
|
-
assetId: id
|
|
125
|
-
});
|
|
367
|
+
if (deleted) await this.removeObject(orgId, asset.id, asset.key);
|
|
126
368
|
return deleted;
|
|
127
369
|
}
|
|
370
|
+
async removeObject(orgId, assetId, key) {
|
|
371
|
+
try {
|
|
372
|
+
await this.storage.remove(key);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
this.logger.warn("stored object not removed", {
|
|
375
|
+
orgId,
|
|
376
|
+
assetId,
|
|
377
|
+
err: error
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
128
381
|
};
|
|
129
382
|
//#endregion
|
|
130
|
-
export { AssetsServiceImpl };
|
|
383
|
+
export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf };
|
|
131
384
|
|
|
132
385
|
//# sourceMappingURL=service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["// assets context — service. Owns org-scoped key construction and the asset\n// lifecycle (register → upload → confirm → serve → remove); delegates the\n// actual object operations to the ObjectStorage port. Org isolation is enforced\n// by scoping every lookup to the caller's org (cross-org reads return null).\nimport { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport type {\n Asset,\n AssetsQuery,\n DownloadTicket,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type { AssetsRepository, AssetsService, AssetsUnitOfWork } from './ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const key = `${orgPrefix(orgId)}${input.kind}/${id}/${sanitizeFilename(input.filename)}`;\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind: input.kind,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', { orgId, assetId: id, kind: input.kind });\n return {\n asset,\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(orgId: string, id: string): Promise<Asset | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: id });\n return asset; // not uploaded yet — stays pending\n }\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.update(id, {\n size: stat.size,\n contentType: stat.contentType ?? asset.contentType,\n status: 'ready',\n });\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n this.logger.info('asset confirmed', { orgId, assetId: id });\n return updated;\n }\n\n list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Asset | null> {\n return this.repo.findById(orgId, id);\n }\n\n async requestDownload(\n orgId: string,\n id: string,\n filename?: string,\n expiresInSeconds?: number,\n ): Promise<DownloadTicket | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const url = await this.storage.presignDownload({\n key: asset.key,\n downloadFilename: filename ?? asset.filename,\n ...(expiresInSeconds === undefined ? {} : { expiresInSeconds }),\n });\n return { url, asset };\n }\n\n async remove(orgId: string, id: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return false;\n }\n await this.storage.remove(asset.key);\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n this.logger.info('asset removed', { orgId, assetId: id });\n return deleted;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport { ConflictError, ForbiddenError, InvalidInputError } from '../shared/errors.js';\nimport type {\n Asset,\n AssetKind,\n AssetView,\n AssetsQuery,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type {\n AssetsRepository,\n AssetsService,\n AssetsUnitOfWork,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport type { FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\nconst MEMBER_IMAGE_REFUSAL = 'A member upload must be a PNG, JPEG, GIF, WebP or AVIF image';\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function kindOf(contentType: string): AssetKind {\n const type = contentType.trim().toLowerCase();\n if (type.startsWith('image/')) {\n return 'image';\n }\n if (type.startsWith('video/')) {\n return 'video';\n }\n if (type.startsWith('audio/')) {\n return 'audio';\n }\n return 'document';\n}\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nfunction storedKey(orgId: string, folder: string, id: string, filename: string): string {\n return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;\n}\n\nfunction finalMemberKey(orgId: string, asset: Asset): string {\n return `${orgPrefix(orgId)}image/${asset.id}/${sanitizeFilename(asset.filename)}`;\n}\n\nfunction graceCutoff(now: Date): Date {\n return new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n memberImageMaxBytes: number;\n downloadLinkExpirySeconds: number;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly memberImageMaxBytes: number;\n private readonly downloadLinkExpirySeconds: number;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) {\n throw new Error(\n `invalid memberImageMaxBytes \"${params.memberImageMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (\n !Number.isSafeInteger(params.downloadLinkExpirySeconds) ||\n params.downloadLinkExpirySeconds <= 0\n ) {\n throw new Error(\n `invalid downloadLinkExpirySeconds \"${params.downloadLinkExpirySeconds}\" (expected a positive integer)`,\n );\n }\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.memberImageMaxBytes = params.memberImageMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const kind = kindOf(input.contentType);\n const folder = input.source === 'member' ? 'uploads' : kind;\n const key = storedKey(orgId, folder, id, input.filename);\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind,\n source: input.source,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', {\n orgId,\n assetId: id,\n kind,\n source: input.source,\n });\n return {\n asset: { ...asset, url: null, downloadUrl: null },\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async requestMemberUpload(\n orgId: string,\n orgUserId: string,\n input: MemberUploadInput,\n ): Promise<UploadTicket> {\n if (!isMemberImageType(input.contentType)) {\n throw new InvalidInputError(MEMBER_IMAGE_REFUSAL);\n }\n return this.requestUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: orgUserId,\n });\n }\n\n async confirm(orgId: string, id: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n if (asset.source === 'member') {\n throw new ForbiddenError('Only the uploader can finalize a member upload');\n }\n const confirmed = await this.finalize(orgId, asset);\n return confirmed ? this.view(confirmed) : null;\n }\n\n async confirmOwn(orgId: string, id: string, orgUserId: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== orgUserId) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId });\n return null;\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n return null;\n }\n return confirmed.status === 'ready'\n ? { ...confirmed, url: this.fileUrls.memberUpload(confirmed.id), downloadUrl: null }\n : { ...confirmed, url: null, downloadUrl: null };\n }\n\n async ownUploadFile(orgId: string, assetId: string, orgUserId: string): Promise<string | null> {\n if (!(await this.ownsImage(orgId, assetId, orgUserId))) {\n this.logger.warn('upload file not served — not the uploader', { orgId, assetId, orgUserId });\n return null;\n }\n return this.signFile(orgId, assetId);\n }\n\n async ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.kind === 'image' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async libraryAssetsAmong(orgId: string, assetIds: string[]): Promise<string[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Set(\n rows.filter((a) => a.source === 'library' && a.status === 'ready').map((a) => a.id),\n );\n return assetIds.filter((id) => library.has(id));\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<AssetView>> {\n const page = await this.repo.list(orgId, { ...query, source: query.source ?? 'library' });\n return { ...page, rows: await Promise.all(page.rows.map((asset) => this.view(asset))) };\n }\n\n async get(orgId: string, id: string): Promise<AssetView | null> {\n const asset = await this.repo.findById(orgId, id);\n return asset ? this.view(asset) : null;\n }\n\n async readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>> {\n const ready = await this.readyAmong(orgId, assetIds);\n return Object.fromEntries(\n ready.map((asset) => [\n asset.id,\n { filename: asset.filename, contentType: asset.contentType, size: asset.size },\n ]),\n );\n }\n\n async signFile(\n orgId: string,\n assetId: string,\n opts: { saveAs?: string } = {},\n ): Promise<string | null> {\n const [asset] = await this.readyAmong(orgId, [assetId]);\n if (!asset) {\n this.logger.warn('file not served — no ready asset', { orgId, assetId });\n return null;\n }\n return opts.saveAs === undefined\n ? this.storage.presignDownload({ key: asset.key })\n : this.storage.presignDownload({\n key: asset.key,\n downloadFilename: opts.saveAs,\n expiresInSeconds: this.downloadLinkExpirySeconds,\n });\n }\n\n async libraryFile(\n orgId: string,\n assetId: string,\n opts: { download: boolean },\n ): Promise<string | null> {\n const asset = await this.repo.findById(orgId, assetId);\n if (!asset || asset.source !== 'library') {\n this.logger.warn('library file not served — not a library asset', { orgId, assetId });\n return null;\n }\n return this.signFile(orgId, assetId, opts.download ? { saveAs: asset.filename } : {});\n }\n\n async remove(orgId: string, id: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return false;\n }\n const removed = await this.discard(orgId, asset);\n if (removed) {\n this.logger.info('asset removed', { orgId, assetId: id });\n }\n return removed;\n }\n\n async removeOwn(orgId: string, id: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== orgUserId) {\n return false;\n }\n return this.remove(orgId, id);\n }\n\n async hasUnattached(orgId: string, now: Date): Promise<boolean> {\n if (await this.repo.hasUnreferencedMemberUploads(orgId, graceCutoff(now))) {\n return true;\n }\n return (await this.strayUploads(orgId, now)).length > 0;\n }\n\n async removeUnattached(orgId: string, now: Date): Promise<number> {\n const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));\n let removed = 0;\n let failed = 0;\n for (const asset of stale) {\n try {\n if (await this.discard(orgId, asset)) {\n removed += 1;\n }\n } catch (err) {\n if (!(err instanceof ConflictError)) {\n failed += 1;\n this.logger.error('unattached upload not removed', { orgId, assetId: asset.id, err });\n }\n }\n }\n let stray = 0;\n for (const key of await this.strayUploads(orgId, now)) {\n try {\n await this.storage.remove(key);\n stray += 1;\n } catch (err) {\n failed += 1;\n this.logger.warn('stray upload not removed', { orgId, err });\n }\n }\n if (removed > 0 || stray > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', { orgId, count: removed, stray, failed });\n }\n return removed + stray;\n }\n\n private async strayUploads(orgId: string, now: Date): Promise<string[]> {\n const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1000;\n const objects = await this.storage.list(`${orgPrefix(orgId)}uploads/`);\n const old = objects\n .filter((o) => o.lastModified !== undefined && Date.parse(o.lastModified) < cutoff)\n .map((o) => o.key);\n if (old.length === 0) {\n return [];\n }\n const held = new Set(await this.repo.heldKeys(orgId, old));\n return old.filter((key) => !held.has(key));\n }\n\n private async finalize(orgId: string, asset: Asset): Promise<Asset | null> {\n if (asset.status !== 'pending') {\n return asset;\n }\n if (asset.source === 'member') {\n return this.finalizeMember(orgId, asset);\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const contentType = stat.contentType ?? asset.contentType;\n const ready = await this.markReady(orgId, asset, {\n key: asset.key,\n kind: kindOf(contentType),\n size: stat.size,\n contentType,\n });\n return ready ?? this.repo.findById(orgId, asset.id);\n }\n\n private async finalizeMember(orgId: string, asset: Asset): Promise<Asset | null> {\n if (!(await this.storage.stat(asset.key))) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const key = finalMemberKey(orgId, asset);\n await this.storage.copy({ from: asset.key, to: key });\n const stat = await this.storage.stat(key);\n if (!stat) {\n throw new Error('copied upload not found');\n }\n const contentType = asset.contentType;\n const kind = kindOf(contentType);\n const reason = !isMemberImageType(contentType)\n ? MEMBER_IMAGE_REFUSAL\n : stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > this.memberImageMaxBytes\n ? 'This image is too large'\n : null;\n if (reason) {\n await this.removeObject(orgId, asset.id, key);\n await this.discard(orgId, asset);\n this.logger.info('member upload refused', { orgId, assetId: asset.id, reason });\n throw new InvalidInputError(reason);\n }\n const ready = await this.markReady(orgId, asset, { key, kind, size: stat.size, contentType });\n if (!ready) {\n return this.repo.findById(orgId, asset.id);\n }\n await this.removeObject(orgId, asset.id, asset.key);\n return ready;\n }\n\n private async markReady(\n orgId: string,\n asset: Asset,\n patch: Pick<Asset, 'key' | 'kind' | 'size' | 'contentType'>,\n ): Promise<Asset | null> {\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.markReady(orgId, asset.id, patch);\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n if (updated) {\n this.logger.info('asset confirmed', { orgId, assetId: asset.id });\n }\n return updated;\n }\n\n private async readyAmong(orgId: string, assetIds: readonly string[]): Promise<Asset[]> {\n const ids = [...new Set(assetIds)];\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, ids);\n return rows.filter((asset) => asset.orgId === orgId && asset.status === 'ready');\n }\n\n private view(asset: Asset): AssetView {\n if (asset.source !== 'library' || asset.status !== 'ready') {\n return { ...asset, url: null, downloadUrl: null };\n }\n return {\n ...asset,\n url: this.fileUrls.libraryFile(asset.id),\n downloadUrl: this.fileUrls.libraryDownload(asset.id),\n };\n }\n\n private async discard(orgId: string, asset: Asset): Promise<boolean> {\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(orgId, asset.id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n if (deleted) {\n await this.removeObject(orgId, asset.id, asset.key);\n }\n return deleted;\n }\n\n private async removeObject(orgId: string, assetId: string, key: string): Promise<void> {\n try {\n await this.storage.remove(key);\n } catch (error) {\n this.logger.warn('stored object not removed', { orgId, assetId, err: error });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
import { DomainEvent } from "../types/shared.js";
|
|
2
|
-
import { EmailTemplateId
|
|
2
|
+
import { EmailTemplateId } from "../types/email-templates.js";
|
|
3
3
|
import { AutomationAction } from "../types/schemas/automations.js";
|
|
4
|
-
import { WebhookSender } from "../types/automations.js";
|
|
5
4
|
import "../types/index.js";
|
|
6
|
-
import
|
|
7
|
-
import { Mailer, MailerLookups } from "../shared/mailer.js";
|
|
5
|
+
import "../shared/ports.js";
|
|
8
6
|
import "./model.js";
|
|
9
|
-
import "./
|
|
7
|
+
import { ActionDeps, EmailRecipient } from "./community-mail.js";
|
|
8
|
+
import { EmailDerivation } from "./community-derivations.js";
|
|
10
9
|
//#region src/automations/actions.d.ts
|
|
11
|
-
|
|
12
|
-
mailer: Pick<Mailer, 'send'>;
|
|
13
|
-
lookups: MailerLookups;
|
|
14
|
-
credentials: Pick<CredentialStore, 'reveal'>;
|
|
15
|
-
webhooks: WebhookSender;
|
|
16
|
-
}
|
|
17
|
-
interface SendEmailDerivation<K extends EmailTemplateId> {
|
|
18
|
-
/** The only event type this template's params can be derived from. */
|
|
19
|
-
trigger: string;
|
|
20
|
-
/** Undefined = the recipient or content behind the event's ids cannot be resolved. */
|
|
21
|
-
derive(event: DomainEvent, lookups: MailerLookups): Promise<{
|
|
22
|
-
to: string;
|
|
23
|
-
params: EmailTemplateParams[K];
|
|
24
|
-
} | undefined>;
|
|
25
|
-
}
|
|
26
|
-
type SendEmailDerivations = { [K in EmailTemplateId]?: SendEmailDerivation<K>; };
|
|
10
|
+
type SendEmailDerivations = { [K in EmailTemplateId]?: EmailDerivation<K>; };
|
|
27
11
|
declare const SEND_EMAIL_DERIVATIONS: SendEmailDerivations;
|
|
28
|
-
/**
|
|
29
|
-
|
|
12
|
+
/** Who an action reached, and who a delivery to failed for (a partial failure does not throw). */
|
|
13
|
+
interface ActionOutcome {
|
|
14
|
+
recipients: string[];
|
|
15
|
+
failed: string[];
|
|
16
|
+
}
|
|
17
|
+
/** Throws on any failure but a partial fan-out — the engine owns retry policy and failure bookkeeping. */
|
|
18
|
+
declare function executeAction(action: AutomationAction, event: DomainEvent, deps: ActionDeps): Promise<ActionOutcome>;
|
|
30
19
|
//#endregion
|
|
31
|
-
export { ActionDeps, SEND_EMAIL_DERIVATIONS, executeAction };
|
|
20
|
+
export { type ActionDeps, ActionOutcome, type EmailRecipient, SEND_EMAIL_DERIVATIONS, executeAction };
|
|
32
21
|
//# sourceMappingURL=actions.d.ts.map
|