@headless-lms/core 0.5.0 → 0.7.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 +18 -18
- package/dist/assets/file-urls.d.ts +3 -0
- package/dist/assets/file-urls.js +1 -0
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/assets/ports.d.ts +1 -1
- package/dist/assets/service.d.ts +1 -1
- package/dist/assets/service.js +28 -23
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/catalog.js +1 -1
- package/dist/automations/index.d.ts +4 -4
- package/dist/automations/notifications/delivery.d.ts +3 -3
- package/dist/automations/notifications/delivery.js +1 -1
- package/dist/automations/notifications/delivery.js.map +1 -1
- package/dist/automations/notifications/processor.js +2 -2
- package/dist/automations/notifications/service.js +1 -1
- package/dist/automations/ports.d.ts +2 -2
- package/dist/automations/service.d.ts +1 -8
- package/dist/automations/service.js +1 -9
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +97 -61
- package/dist/content/index.d.ts +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.js +1 -1
- package/dist/content/ports.d.ts +11 -11
- package/dist/content/service.d.ts +16 -12
- package/dist/content/service.js +62 -16
- package/dist/content/service.js.map +1 -1
- package/dist/discussion/chats.js +2 -2
- package/dist/discussion/events.d.ts +8 -8
- package/dist/discussion/posts.d.ts +1 -1
- package/dist/discussion/posts.js +3 -3
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.js +2 -2
- package/dist/discussion/subscriptions.js +1 -1
- package/dist/entitlements/events.d.ts +48 -48
- package/dist/entitlements/service.js +1 -1
- package/dist/identity/devices.js +1 -1
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/notification-settings.js +1 -1
- package/dist/identity/service.d.ts +1 -1
- package/dist/identity/service.js +3 -3
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/organizations/effective-permissions.d.ts +3 -1
- package/dist/organizations/effective-permissions.js +8 -3
- package/dist/organizations/effective-permissions.js.map +1 -1
- package/dist/organizations/events.d.ts +29 -23
- package/dist/organizations/index.d.ts +2 -2
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-categories.js +1 -1
- package/dist/organizations/ports.d.ts +6 -3
- package/dist/organizations/service.d.ts +1 -1
- package/dist/organizations/service.js +17 -7
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.js +1 -1
- package/dist/organizations/testing.js.map +1 -1
- package/dist/progress/service.js +2 -2
- package/dist/reporting/member/model.d.ts +3 -0
- package/dist/reporting/member/ports.d.ts +2 -1
- package/dist/reporting/member/service.d.ts +2 -1
- package/dist/reporting/member/service.js +17 -1
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +1 -0
- package/dist/types/automations.d.ts +5 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/index.d.ts +4 -4
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/schemas/content.d.ts +70 -8
- package/dist/types/schemas/content.js +19 -2
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +14 -14
- package/dist/types/schemas/entitlements.d.ts +8 -8
- package/dist/types/schemas/index.d.ts +3 -3
- package/dist/types/schemas/index.js +3 -3
- package/dist/types/schemas/notifications.d.ts +4 -4
- package/dist/types/schemas/organizations.d.ts +4 -1
- package/dist/types/schemas/organizations.js +3 -1
- package/dist/types/schemas/organizations.js.map +1 -1
- package/package.json +1 -1
package/dist/assets/events.d.ts
CHANGED
|
@@ -39,20 +39,20 @@ declare const assetEvents: {
|
|
|
39
39
|
audio: "audio";
|
|
40
40
|
document: "document";
|
|
41
41
|
}>;
|
|
42
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
43
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
44
|
-
status: import("zod").ZodEnum<{
|
|
45
|
-
pending: "pending";
|
|
46
|
-
ready: "ready";
|
|
47
|
-
}>;
|
|
48
42
|
source: import("zod").ZodEnum<{
|
|
49
43
|
library: "library";
|
|
50
44
|
member: "member";
|
|
51
45
|
}>;
|
|
46
|
+
status: import("zod").ZodEnum<{
|
|
47
|
+
pending: "pending";
|
|
48
|
+
ready: "ready";
|
|
49
|
+
}>;
|
|
52
50
|
uploadedBy: import("zod").ZodString;
|
|
53
51
|
filename: import("zod").ZodString;
|
|
54
52
|
contentType: import("zod").ZodString;
|
|
55
53
|
size: import("zod").ZodNumber;
|
|
54
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
55
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
56
56
|
}, import("zod/v4/core").$strip>>;
|
|
57
57
|
assetReady: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
58
58
|
orgId: string;
|
|
@@ -91,20 +91,20 @@ declare const assetEvents: {
|
|
|
91
91
|
audio: "audio";
|
|
92
92
|
document: "document";
|
|
93
93
|
}>;
|
|
94
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
95
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
96
|
-
status: import("zod").ZodEnum<{
|
|
97
|
-
pending: "pending";
|
|
98
|
-
ready: "ready";
|
|
99
|
-
}>;
|
|
100
94
|
source: import("zod").ZodEnum<{
|
|
101
95
|
library: "library";
|
|
102
96
|
member: "member";
|
|
103
97
|
}>;
|
|
98
|
+
status: import("zod").ZodEnum<{
|
|
99
|
+
pending: "pending";
|
|
100
|
+
ready: "ready";
|
|
101
|
+
}>;
|
|
104
102
|
uploadedBy: import("zod").ZodString;
|
|
105
103
|
filename: import("zod").ZodString;
|
|
106
104
|
contentType: import("zod").ZodString;
|
|
107
105
|
size: import("zod").ZodNumber;
|
|
106
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
107
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
108
108
|
}, import("zod/v4/core").$strip>>;
|
|
109
109
|
assetDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
110
110
|
orgId: string;
|
|
@@ -143,20 +143,20 @@ declare const assetEvents: {
|
|
|
143
143
|
audio: "audio";
|
|
144
144
|
document: "document";
|
|
145
145
|
}>;
|
|
146
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
147
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
148
|
-
status: import("zod").ZodEnum<{
|
|
149
|
-
pending: "pending";
|
|
150
|
-
ready: "ready";
|
|
151
|
-
}>;
|
|
152
146
|
source: import("zod").ZodEnum<{
|
|
153
147
|
library: "library";
|
|
154
148
|
member: "member";
|
|
155
149
|
}>;
|
|
150
|
+
status: import("zod").ZodEnum<{
|
|
151
|
+
pending: "pending";
|
|
152
|
+
ready: "ready";
|
|
153
|
+
}>;
|
|
156
154
|
uploadedBy: import("zod").ZodString;
|
|
157
155
|
filename: import("zod").ZodString;
|
|
158
156
|
contentType: import("zod").ZodString;
|
|
159
157
|
size: import("zod").ZodNumber;
|
|
158
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
159
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
160
160
|
}, import("zod/v4/core").$strip>>;
|
|
161
161
|
};
|
|
162
162
|
type AssetCreated = EventOf<typeof assetEvents.assetCreated>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SpaceImage } from "../types/schemas/content.js";
|
|
2
|
+
import "../types/index.js";
|
|
1
3
|
//#region src/assets/file-urls.d.ts
|
|
2
4
|
type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';
|
|
3
5
|
/** URLs for every file a response refers to. A public image (a profile photo, or the
|
|
@@ -6,6 +8,7 @@ type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';
|
|
|
6
8
|
interface FileUrls {
|
|
7
9
|
publicImage(orgId: string, ownerId: string, assetId: string): string;
|
|
8
10
|
thumbnail(owner: ThumbnailOwner, ownerId: string): string;
|
|
11
|
+
spaceImage(spaceId: string, image: SpaceImage): string;
|
|
9
12
|
activityAsset(activityId: string, assetId: string): string;
|
|
10
13
|
downloadFile(downloadId: string, assetId: string): string;
|
|
11
14
|
eventFile(eventId: string, assetId: string): string;
|
package/dist/assets/file-urls.js
CHANGED
|
@@ -18,6 +18,7 @@ function createFileUrls(apiUrl, publicUrl) {
|
|
|
18
18
|
return {
|
|
19
19
|
publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),
|
|
20
20
|
thumbnail: (owner, ownerId) => at("member", THUMBNAIL_PATHS[owner], ownerId, "thumbnail"),
|
|
21
|
+
spaceImage: (spaceId, image) => at("member", "spaces", spaceId, "images", image),
|
|
21
22
|
activityAsset: (activityId, assetId) => at("member", "activities", activityId, "assets", assetId),
|
|
22
23
|
downloadFile: (downloadId, assetId) => at("member", "downloads", downloadId, "assets", assetId),
|
|
23
24
|
eventFile: (eventId, assetId) => at("member", "events", eventId, "assets", assetId),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX } from '../types/index.js';\n\nexport type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';\n\n/** URLs for every file a response refers to. A public image (a profile photo, or the\n * thumbnail or cover of a published item) is a public storage URL; every other file is a\n * stable, access-checked URL on the API that redirects to a signed storage URL. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n thumbnail(owner: ThumbnailOwner, ownerId: string): string;\n activityAsset(activityId: string, assetId: string): string;\n downloadFile(downloadId: string, assetId: string): string;\n eventFile(eventId: string, assetId: string): string;\n postCover(postId: string): string;\n postAsset(postId: string, assetId: string): string;\n messageAttachment(chatId: string, messageId: string, assetId: string): string;\n memberUpload(assetId: string): string;\n libraryFile(assetId: string): string;\n libraryDownload(assetId: string): string;\n}\n\nconst THUMBNAIL_PATHS: Record<ThumbnailOwner, string> = {\n course: 'courses',\n download: 'downloads',\n community: 'communities',\n event: 'events',\n};\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls {\n const base = apiUrl.replace(/\\/+$/, '');\n const at = (...segments: string[]) =>\n `${base}/api/${segments.map((s) => encodeURIComponent(s)).join('/')}`;\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n thumbnail: (owner, ownerId) => at('member', THUMBNAIL_PATHS[owner], ownerId, 'thumbnail'),\n activityAsset: (activityId, assetId) =>\n at('member', 'activities', activityId, 'assets', assetId),\n downloadFile: (downloadId, assetId) => at('member', 'downloads', downloadId, 'assets', assetId),\n eventFile: (eventId, assetId) => at('member', 'events', eventId, 'assets', assetId),\n postCover: (postId) => at('member', 'posts', postId, 'cover'),\n postAsset: (postId, assetId) => at('member', 'posts', postId, 'assets', assetId),\n messageAttachment: (chatId, messageId, assetId) =>\n at('member', 'chats', chatId, 'messages', messageId, 'attachments', assetId),\n memberUpload: (assetId) => at('member', 'uploads', assetId, 'file'),\n libraryFile: (assetId) => at('assets', assetId, 'file'),\n libraryDownload: (assetId) => at('assets', assetId, 'download'),\n };\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX, type SpaceImage } from '../types/index.js';\n\nexport type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';\n\n/** URLs for every file a response refers to. A public image (a profile photo, or the\n * thumbnail or cover of a published item) is a public storage URL; every other file is a\n * stable, access-checked URL on the API that redirects to a signed storage URL. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n thumbnail(owner: ThumbnailOwner, ownerId: string): string;\n spaceImage(spaceId: string, image: SpaceImage): string;\n activityAsset(activityId: string, assetId: string): string;\n downloadFile(downloadId: string, assetId: string): string;\n eventFile(eventId: string, assetId: string): string;\n postCover(postId: string): string;\n postAsset(postId: string, assetId: string): string;\n messageAttachment(chatId: string, messageId: string, assetId: string): string;\n memberUpload(assetId: string): string;\n libraryFile(assetId: string): string;\n libraryDownload(assetId: string): string;\n}\n\nconst THUMBNAIL_PATHS: Record<ThumbnailOwner, string> = {\n course: 'courses',\n download: 'downloads',\n community: 'communities',\n event: 'events',\n};\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls {\n const base = apiUrl.replace(/\\/+$/, '');\n const at = (...segments: string[]) =>\n `${base}/api/${segments.map((s) => encodeURIComponent(s)).join('/')}`;\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n thumbnail: (owner, ownerId) => at('member', THUMBNAIL_PATHS[owner], ownerId, 'thumbnail'),\n spaceImage: (spaceId, image) => at('member', 'spaces', spaceId, 'images', image),\n activityAsset: (activityId, assetId) =>\n at('member', 'activities', activityId, 'assets', assetId),\n downloadFile: (downloadId, assetId) => at('member', 'downloads', downloadId, 'assets', assetId),\n eventFile: (eventId, assetId) => at('member', 'events', eventId, 'assets', assetId),\n postCover: (postId) => at('member', 'posts', postId, 'cover'),\n postAsset: (postId, assetId) => at('member', 'posts', postId, 'assets', assetId),\n messageAttachment: (chatId, messageId, assetId) =>\n at('member', 'chats', chatId, 'messages', messageId, 'attachments', assetId),\n memberUpload: (assetId) => at('member', 'uploads', assetId, 'file'),\n libraryFile: (assetId) => at('assets', assetId, 'file'),\n libraryDownload: (assetId) => at('assets', assetId, 'download'),\n };\n}\n"],"mappings":";;AAsBA,MAAM,kBAAkD;CACtD,QAAQ;CACR,UAAU;CACV,WAAW;CACX,OAAO;AACT;AAEA,SAAgB,kBAAkB,OAAe,SAAyB;CACxE,OAAO,GAAG,gBAAgB,MAAM,GAAG,QAAQ;AAC7C;AAEA,SAAgB,eAAe,OAAe,SAAiB,SAAyB;CACtF,OAAO,GAAG,kBAAkB,OAAO,OAAO,IAAI;AAChD;AAEA,SAAgB,eAAe,QAAgB,WAA8C;CAC3F,MAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE;CACtC,MAAM,MAAM,GAAG,aACb,GAAG,KAAK,OAAO,SAAS,KAAK,MAAM,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;CACpE,OAAO;EACL,cAAc,OAAO,SAAS,YAAY,UAAU,eAAe,OAAO,SAAS,OAAO,CAAC;EAC3F,YAAY,OAAO,YAAY,GAAG,UAAU,gBAAgB,QAAQ,SAAS,WAAW;EACxF,aAAa,SAAS,UAAU,GAAG,UAAU,UAAU,SAAS,UAAU,KAAK;EAC/E,gBAAgB,YAAY,YAC1B,GAAG,UAAU,cAAc,YAAY,UAAU,OAAO;EAC1D,eAAe,YAAY,YAAY,GAAG,UAAU,aAAa,YAAY,UAAU,OAAO;EAC9F,YAAY,SAAS,YAAY,GAAG,UAAU,UAAU,SAAS,UAAU,OAAO;EAClF,YAAY,WAAW,GAAG,UAAU,SAAS,QAAQ,OAAO;EAC5D,YAAY,QAAQ,YAAY,GAAG,UAAU,SAAS,QAAQ,UAAU,OAAO;EAC/E,oBAAoB,QAAQ,WAAW,YACrC,GAAG,UAAU,SAAS,QAAQ,YAAY,WAAW,eAAe,OAAO;EAC7E,eAAe,YAAY,GAAG,UAAU,WAAW,SAAS,MAAM;EAClE,cAAc,YAAY,GAAG,UAAU,SAAS,MAAM;EACtD,kBAAkB,YAAY,GAAG,UAAU,SAAS,UAAU;CAChE;AACF"}
|
package/dist/assets/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
2
1
|
import { assetEvents } from "./events.js";
|
|
2
|
+
import { createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
3
3
|
import { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf, memberKindOf } from "./service.js";
|
|
4
4
|
import { UPLOADS_REMOVE_UNATTACHED_JOB, assetsJobs, assetsRecurringJobs } from "./jobs.js";
|
|
5
5
|
export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, UPLOADS_REMOVE_UNATTACHED_JOB, assetEvents, assetsJobs, assetsRecurringJobs, createFileUrls, isMemberImageType, kindOf, memberKindOf, publicImageKey, publicImagePrefix };
|
package/dist/assets/ports.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface AssetFiles {
|
|
|
39
39
|
signFile(orgId: string, assetId: string, opts?: {
|
|
40
40
|
saveAs?: string;
|
|
41
41
|
}): Promise<string | null>;
|
|
42
|
-
|
|
42
|
+
syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
|
|
43
43
|
}
|
|
44
44
|
interface MemberUploadInput {
|
|
45
45
|
filename: string;
|
package/dist/assets/service.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ declare class AssetsServiceImpl implements AssetsService {
|
|
|
50
50
|
signFile(orgId: string, assetId: string, opts?: {
|
|
51
51
|
saveAs?: string;
|
|
52
52
|
}): Promise<string | null>;
|
|
53
|
-
|
|
53
|
+
syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
|
|
54
54
|
libraryFile(assetId: string, opts: {
|
|
55
55
|
download: boolean;
|
|
56
56
|
}, actor: Actor): Promise<string>;
|
package/dist/assets/service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
3
|
-
import { genId } from "../shared/id.js";
|
|
1
|
+
import { assetEvents } from "./events.js";
|
|
4
2
|
import { PUBLIC_PREFIX } from "../types/ports.js";
|
|
5
3
|
import { publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
6
|
-
import {
|
|
4
|
+
import { genId } from "../shared/id.js";
|
|
5
|
+
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
6
|
+
import { noopLogger } from "../shared/logger.js";
|
|
7
7
|
//#region src/assets/service.ts
|
|
8
8
|
const WRITE_PERMISSION = {
|
|
9
9
|
course: "courses:write",
|
|
@@ -264,35 +264,40 @@ var AssetsServiceImpl = class {
|
|
|
264
264
|
expiresInSeconds: this.downloadLinkExpirySeconds
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
|
-
async
|
|
267
|
+
async syncPublicImages(orgId, ownerId, assetIds) {
|
|
268
268
|
try {
|
|
269
|
-
const wanted =
|
|
269
|
+
const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));
|
|
270
270
|
const held = await this.storage.list(publicImagePrefix(orgId, ownerId));
|
|
271
|
-
for (const object of held) if (object.key
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
|
|
271
|
+
for (const object of held) if (!wanted.has(object.key)) await this.storage.remove(object.key);
|
|
272
|
+
const heldKeys = new Set(held.map((object) => object.key));
|
|
273
|
+
const missing = [...wanted].filter(([key]) => !heldKeys.has(key));
|
|
274
|
+
if (missing.length === 0) return;
|
|
275
|
+
const ready = new Map((await this.readyAmong(orgId, missing.map(([, id]) => id))).map((asset) => [asset.id, asset]));
|
|
276
|
+
for (const [key, assetId] of missing) {
|
|
277
|
+
const asset = ready.get(assetId);
|
|
278
|
+
if (!asset || asset.kind !== "image") {
|
|
279
|
+
this.logger.warn("public image not published — no ready image", {
|
|
280
|
+
orgId,
|
|
281
|
+
ownerId,
|
|
282
|
+
assetId
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
await this.storage.publish({
|
|
287
|
+
from: asset.key,
|
|
288
|
+
to: key
|
|
289
|
+
});
|
|
290
|
+
this.logger.info("public image published", {
|
|
276
291
|
orgId,
|
|
277
292
|
ownerId,
|
|
278
293
|
assetId
|
|
279
294
|
});
|
|
280
|
-
return;
|
|
281
295
|
}
|
|
282
|
-
await this.storage.publish({
|
|
283
|
-
from: asset.key,
|
|
284
|
-
to: wanted
|
|
285
|
-
});
|
|
286
|
-
this.logger.info("public image published", {
|
|
287
|
-
orgId,
|
|
288
|
-
ownerId,
|
|
289
|
-
assetId
|
|
290
|
-
});
|
|
291
296
|
} catch (err) {
|
|
292
|
-
this.logger.error("public
|
|
297
|
+
this.logger.error("public images not synced", {
|
|
293
298
|
orgId,
|
|
294
299
|
ownerId,
|
|
295
|
-
|
|
300
|
+
assetIds,
|
|
296
301
|
err
|
|
297
302
|
});
|
|
298
303
|
}
|
|
@@ -1 +1 @@
|
|
|
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 { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.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 MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix, type FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\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)}${asset.kind}/${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 memberFileMaxBytes: 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 memberFileMaxBytes: 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 (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (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.memberFileMaxBytes = params.memberFileMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : 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 contentId: input.contentId,\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 contentId: input.contentId,\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 confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\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 if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\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(assetId: string, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n if (!(await this.ownsUpload(orgId, assetId, actor.id))) {\n this.logger.warn('upload file not served — not the uploader', {\n orgId,\n assetId,\n orgUserId: actor.id,\n });\n throw new NotFoundError('Upload', assetId);\n }\n const url = await this.signFile(orgId, assetId);\n if (!url) {\n throw new NotFoundError('Upload', assetId);\n }\n return url;\n }\n\n async ownsUpload(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.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\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 libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\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(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\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 syncPublicImage(orgId: string, ownerId: string, assetId: string | null): Promise<void> {\n try {\n const wanted = assetId === null ? null : publicImageKey(orgId, ownerId, assetId);\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (object.key !== wanted) {\n await this.storage.remove(object.key);\n }\n }\n if (wanted === null || held.some((object) => object.key === wanted)) {\n return;\n }\n const [asset] = await this.readyAmong(orgId, [assetId!]);\n if (!asset || asset.kind !== 'image') {\n this.logger.warn('public image not published — no ready image', {\n orgId,\n ownerId,\n assetId,\n });\n return;\n }\n await this.storage.publish({ from: asset.key, to: wanted });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n } catch (err) {\n this.logger.error('public image not synced', { orgId, ownerId, assetId, err });\n }\n }\n\n async libraryFile(assetId: string, opts: { download: boolean }, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\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 throw new NotFoundError('Asset', assetId);\n }\n await this.requireReach(asset, actor);\n const url = await this.signFile(\n orgId,\n assetId,\n opts.download ? { saveAs: asset.filename } : {},\n );\n if (!url) {\n throw new NotFoundError('Asset', assetId);\n }\n return url;\n }\n\n async remove(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('media:delete');\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n async removeOwn(id: string, actor: Actor): Promise<void> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n throw new NotFoundError('Asset', id);\n }\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n private async removeAsset(orgId: string, id: string, actorId: string): Promise<void> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || !(await this.discard(orgId, asset))) {\n throw new NotFoundError('Asset', id);\n }\n this.logger.info('asset removed', { orgId, assetId: id, actorId });\n }\n\n private async requireReach(asset: Asset, actor: Actor): Promise<void> {\n if (actor.permissions.has('media:read')) {\n return;\n }\n if (asset.contentId === null) {\n this.logger.warn('asset refused', {\n orgId: actor.orgId,\n assetId: asset.id,\n orgUserId: actor.id,\n });\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(actor.orgId, asset.contentId, actor);\n }\n\n private async requireItemWrite(orgId: string, contentId: string, actor: Actor): Promise<void> {\n const type = await this.repo.contentTypeOf(orgId, contentId);\n if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {\n this.logger.warn('asset refused', { orgId, contentId, orgUserId: actor.id });\n throw new ForbiddenError(\n type ? `You need media:read or ${WRITE_PERMISSION[type]}` : 'You need media:read',\n );\n }\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 const orphans = await this.orphanPublicImages(orgId);\n for (const key of orphans) {\n await this.removeObject(orgId, key.split('/').pop() ?? '', key);\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 || orphans.length > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', {\n orgId,\n count: removed,\n stray,\n publicCopies: orphans.length,\n failed,\n });\n }\n return removed + stray + orphans.length;\n }\n\n private async orphanPublicImages(orgId: string): Promise<string[]> {\n const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n if (objects.length === 0) {\n return [];\n }\n const assetIdOf = (key: string) => key.split('/').pop() ?? '';\n const live = new Set(\n (\n await this.readyAmong(\n orgId,\n objects.map((o) => assetIdOf(o.key)),\n )\n ).map((a) => a.id),\n );\n return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);\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 = memberKindOf(contentType);\n const limit = kind === 'image' ? this.memberImageMaxBytes : this.memberFileMaxBytes;\n const reason =\n stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > limit\n ? `This ${kind === 'image' ? 'image' : 'file'} is over the ${sizeLabel(limit)} limit`\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 await this.removePublicCopies(orgId, asset.id);\n }\n return deleted;\n }\n\n private async removePublicCopies(orgId: string, assetId: string): Promise<void> {\n try {\n const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) {\n await this.removeObject(orgId, assetId, copy.key);\n }\n } catch (err) {\n this.logger.warn('public copies not removed', { orgId, assetId, err });\n }\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\nfunction sizeLabel(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\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 { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.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 MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix, type FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\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)}${asset.kind}/${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 memberFileMaxBytes: 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 memberFileMaxBytes: 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 (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (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.memberFileMaxBytes = params.memberFileMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : 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 contentId: input.contentId,\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 contentId: input.contentId,\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 confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\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 if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\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(assetId: string, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n if (!(await this.ownsUpload(orgId, assetId, actor.id))) {\n this.logger.warn('upload file not served — not the uploader', {\n orgId,\n assetId,\n orgUserId: actor.id,\n });\n throw new NotFoundError('Upload', assetId);\n }\n const url = await this.signFile(orgId, assetId);\n if (!url) {\n throw new NotFoundError('Upload', assetId);\n }\n return url;\n }\n\n async ownsUpload(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.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\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 libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\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(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\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 syncPublicImages(\n orgId: string,\n ownerId: string,\n assetIds: readonly string[],\n ): Promise<void> {\n try {\n const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (!wanted.has(object.key)) {\n await this.storage.remove(object.key);\n }\n }\n const heldKeys = new Set(held.map((object) => object.key));\n const missing = [...wanted].filter(([key]) => !heldKeys.has(key));\n if (missing.length === 0) {\n return;\n }\n const ready = new Map(\n (\n await this.readyAmong(\n orgId,\n missing.map(([, id]) => id),\n )\n ).map((asset) => [asset.id, asset]),\n );\n for (const [key, assetId] of missing) {\n const asset = ready.get(assetId);\n if (!asset || asset.kind !== 'image') {\n this.logger.warn('public image not published — no ready image', {\n orgId,\n ownerId,\n assetId,\n });\n continue;\n }\n await this.storage.publish({ from: asset.key, to: key });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n }\n } catch (err) {\n this.logger.error('public images not synced', { orgId, ownerId, assetIds, err });\n }\n }\n\n async libraryFile(assetId: string, opts: { download: boolean }, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\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 throw new NotFoundError('Asset', assetId);\n }\n await this.requireReach(asset, actor);\n const url = await this.signFile(\n orgId,\n assetId,\n opts.download ? { saveAs: asset.filename } : {},\n );\n if (!url) {\n throw new NotFoundError('Asset', assetId);\n }\n return url;\n }\n\n async remove(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('media:delete');\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n async removeOwn(id: string, actor: Actor): Promise<void> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n throw new NotFoundError('Asset', id);\n }\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n private async removeAsset(orgId: string, id: string, actorId: string): Promise<void> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || !(await this.discard(orgId, asset))) {\n throw new NotFoundError('Asset', id);\n }\n this.logger.info('asset removed', { orgId, assetId: id, actorId });\n }\n\n private async requireReach(asset: Asset, actor: Actor): Promise<void> {\n if (actor.permissions.has('media:read')) {\n return;\n }\n if (asset.contentId === null) {\n this.logger.warn('asset refused', {\n orgId: actor.orgId,\n assetId: asset.id,\n orgUserId: actor.id,\n });\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(actor.orgId, asset.contentId, actor);\n }\n\n private async requireItemWrite(orgId: string, contentId: string, actor: Actor): Promise<void> {\n const type = await this.repo.contentTypeOf(orgId, contentId);\n if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {\n this.logger.warn('asset refused', { orgId, contentId, orgUserId: actor.id });\n throw new ForbiddenError(\n type ? `You need media:read or ${WRITE_PERMISSION[type]}` : 'You need media:read',\n );\n }\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 const orphans = await this.orphanPublicImages(orgId);\n for (const key of orphans) {\n await this.removeObject(orgId, key.split('/').pop() ?? '', key);\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 || orphans.length > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', {\n orgId,\n count: removed,\n stray,\n publicCopies: orphans.length,\n failed,\n });\n }\n return removed + stray + orphans.length;\n }\n\n private async orphanPublicImages(orgId: string): Promise<string[]> {\n const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n if (objects.length === 0) {\n return [];\n }\n const assetIdOf = (key: string) => key.split('/').pop() ?? '';\n const live = new Set(\n (\n await this.readyAmong(\n orgId,\n objects.map((o) => assetIdOf(o.key)),\n )\n ).map((a) => a.id),\n );\n return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);\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 = memberKindOf(contentType);\n const limit = kind === 'image' ? this.memberImageMaxBytes : this.memberFileMaxBytes;\n const reason =\n stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > limit\n ? `This ${kind === 'image' ? 'image' : 'file'} is over the ${sizeLabel(limit)} limit`\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 await this.removePublicCopies(orgId, asset.id);\n }\n return deleted;\n }\n\n private async removePublicCopies(orgId: string, assetId: string): Promise<void> {\n try {\n const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) {\n await this.removeObject(orgId, assetId, copy.key);\n }\n } catch (err) {\n this.logger.warn('public copies not removed', { orgId, assetId, err });\n }\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\nfunction sizeLabel(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { entitlementEvents } from "../entitlements/events.js";
|
|
2
1
|
import { assetEvents } from "../assets/events.js";
|
|
2
|
+
import { entitlementEvents } from "../entitlements/events.js";
|
|
3
3
|
import { contentEvents } from "../content/events.js";
|
|
4
4
|
import { discussionEvents } from "../discussion/events.js";
|
|
5
5
|
import { identityEvents } from "../identity/events.js";
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { Page } from "../types/shared.js";
|
|
2
2
|
import { Automation, AutomationAction, AutomationActionResult, AutomationKind, AutomationRun, AutomationRunStatus, AutomationRunsQuery, AutomationTrigger, AutomationsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, CreatedWebhook, UpdateAutomationInput, UpdateWebhookInput, Webhook, WebhookSecret } from "../types/schemas/automations.js";
|
|
3
|
-
import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
|
|
3
|
+
import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, DeliveryExecutor, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
|
|
4
4
|
import { ACTION_FAILED, ActionFailedError, AutomationKindMismatchError, InvalidTriggerError, ReservedActionError, actionFailureMessage } from "./model.js";
|
|
5
5
|
import "./types.js";
|
|
6
6
|
import { AutomationRunsRepository, AutomationsRepository, AutomationsService, AutomationsUnitOfWork, ChatDebounce, ListedNotification, NewAutomation, NewAutomationRun, NewDelivery, NewNotification, NotificationCategories, NotificationPage, NotificationReads, NotificationsRepository, NotificationsService, RecipientRule, UnreadCounts } from "./ports.js";
|
|
7
7
|
import { CommunityMailReads, GrantWriter } from "./community-mail.js";
|
|
8
8
|
import { AutomationActionFailed, AutomationCreated, AutomationDeleted, AutomationDisabled, AutomationEnabled, AutomationEvent, AutomationRunCompleted, AutomationRunFailed, AutomationRunStarted, AutomationUpdated, automationEvents } from "./events.js";
|
|
9
|
-
import { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery } from "./notifications/delivery.js";
|
|
10
|
-
import { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, PUSH } from "./notifications/processor.js";
|
|
11
9
|
import { AutomationsServiceImpl } from "./service.js";
|
|
12
10
|
import { cronMatches } from "./schedule.js";
|
|
13
11
|
import { NotificationsServiceImpl } from "./notifications/service.js";
|
|
12
|
+
import { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery } from "./notifications/delivery.js";
|
|
13
|
+
import { DEVICE_WINDOW_MS, IN_APP, MESSAGE_SENT, NotificationProcessor, PUSH } from "./notifications/processor.js";
|
|
14
14
|
import { CHAT_QUIET_MS, FLUSH_JOB, SchedulerChatDebounce, flushKey, notificationJobs } from "./notifications/debounce.js";
|
|
15
15
|
import { RECIPIENT_RULES } from "./notifications/rules/index.js";
|
|
16
16
|
import { NOTIFICATION_TYPES } from "./notifications/types.js";
|
|
17
17
|
import { DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES } from "./notifications/defaults.js";
|
|
18
18
|
import { applicableCategories } from "./notifications/applicable.js";
|
|
19
|
-
export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
|
|
19
|
+
export { ACTION_FAILED, ActionFailedError, type Automation, type AutomationAction, type AutomationActionFailed, type AutomationActionResult, type AutomationCreated, type AutomationDeleted, type AutomationDisabled, type AutomationDispatch, type AutomationEnabled, type AutomationEngine, type AutomationEvent, type AutomationExecutor, type AutomationKind, AutomationKindMismatchError, type AutomationRun, type AutomationRunCompleted, type AutomationRunFailed, type AutomationRunStarted, type AutomationRunStatus, type AutomationRunsQuery, type AutomationRunsRepository, type AutomationTrigger, type AutomationUpdated, type AutomationsQuery, type AutomationsRepository, type AutomationsService, AutomationsServiceImpl, type AutomationsUnitOfWork, type AvailableAction, type AvailableActions, type AvailableTriggers, CHANNEL_NOT_CONFIGURED, CHAT_QUIET_MS, type ChatDebounce, type CommunityMailReads, type CreateAutomationInput, type CreateWebhookInput, type CreatedWebhook, DEFAULT_COMMUNITY_CATEGORIES, DEFAULT_ORG_CATEGORIES, DEVICE_WINDOW_MS, type DeliveryDispatch, type DeliveryExecutor, FLUSH_JOB, type GrantWriter, IN_APP, InvalidTriggerError, type ListedNotification, MESSAGE_SENT, NOTIFICATION_TYPES, NOT_ACCEPTED, NO_ADDRESS, type NewAutomation, type NewAutomationRun, type NewDelivery, type NewNotification, type NotificationCategories, NotificationDelivery, type NotificationPage, NotificationProcessor, type NotificationReads, type NotificationsRepository, type NotificationsService, NotificationsServiceImpl, PUSH, type Page, RECIPIENT_RULES, type RecipientRule, ReservedActionError, SchedulerChatDebounce, type UnreadCounts, type UpdateAutomationInput, type UpdateWebhookInput, type Webhook, type WebhookRequest, type WebhookResponse, type WebhookSecret, type WebhookSender, actionFailureMessage, applicableCategories, automationEvents, cronMatches, flushKey, notificationJobs };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Logger } from "../../types/ports.js";
|
|
2
|
-
import { DeliveryDispatch } from "../../types/automations.js";
|
|
2
|
+
import { DeliveryDispatch, DeliveryExecutor } from "../../types/automations.js";
|
|
3
3
|
import "../../types/index.js";
|
|
4
4
|
import { ChannelRegistry } from "../../shared/ports.js";
|
|
5
5
|
import { DevicesService } from "../../identity/ports.js";
|
|
@@ -15,13 +15,13 @@ type NotificationDeliveryParams = {
|
|
|
15
15
|
devices: Pick<DevicesService, 'removeDead'>;
|
|
16
16
|
logger?: Logger;
|
|
17
17
|
};
|
|
18
|
-
declare class NotificationDelivery {
|
|
18
|
+
declare class NotificationDelivery implements DeliveryExecutor {
|
|
19
19
|
private readonly repo;
|
|
20
20
|
private readonly channels;
|
|
21
21
|
private readonly devices;
|
|
22
22
|
private readonly logger;
|
|
23
23
|
constructor(params: NotificationDeliveryParams);
|
|
24
|
-
|
|
24
|
+
runDelivery(d: DeliveryDispatch, lastAttempt: boolean): Promise<void>;
|
|
25
25
|
}
|
|
26
26
|
//#endregion
|
|
27
27
|
export { CHANNEL_NOT_CONFIGURED, NOT_ACCEPTED, NO_ADDRESS, NotificationDelivery, NotificationDeliveryParams };
|
|
@@ -17,7 +17,7 @@ var NotificationDelivery = class {
|
|
|
17
17
|
this.devices = params.devices;
|
|
18
18
|
this.logger = params.logger ?? noopLogger;
|
|
19
19
|
}
|
|
20
|
-
async
|
|
20
|
+
async runDelivery(d, lastAttempt) {
|
|
21
21
|
const found = await this.repo.findDelivery(d.orgId, d.deliveryId);
|
|
22
22
|
if (!found || found.delivery.status !== "pending") {
|
|
23
23
|
this.logger.debug("notification delivery skipped", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery.js","names":[],"sources":["../../../src/automations/notifications/delivery.ts"],"sourcesContent":["import type { DevicesService } from '../../identity/index.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport type { ChannelRegistry, Logger } from '../../shared/ports.js';\nimport type { ChannelRecipient, DeliveryDispatch, DeliveryResult } from '../../types/index.js';\nimport type { NotificationsRepository } from '../ports.js';\n\nexport const CHANNEL_NOT_CONFIGURED = 'channel not configured';\nexport const NO_ADDRESS = 'no address';\nexport const NOT_ACCEPTED = 'not accepted';\n\nexport type NotificationDeliveryParams = {\n repo: Pick<NotificationsRepository, 'findDelivery' | 'markSent' | 'recordError' | 'markFailed'>;\n channels: Pick<ChannelRegistry, 'get'>;\n devices: Pick<DevicesService, 'removeDead'>;\n logger?: Logger;\n};\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport class NotificationDelivery {\n private readonly repo: NotificationDeliveryParams['repo'];\n private readonly channels: Pick<ChannelRegistry, 'get'>;\n private readonly devices: Pick<DevicesService, 'removeDead'>;\n private readonly logger: Logger;\n\n constructor(params: NotificationDeliveryParams) {\n this.repo = params.repo;\n this.channels = params.channels;\n this.devices = params.devices;\n this.logger = params.logger ?? noopLogger;\n }\n\n async
|
|
1
|
+
{"version":3,"file":"delivery.js","names":[],"sources":["../../../src/automations/notifications/delivery.ts"],"sourcesContent":["import type { DevicesService } from '../../identity/index.js';\nimport { noopLogger } from '../../shared/logger.js';\nimport type { ChannelRegistry, Logger } from '../../shared/ports.js';\nimport type { ChannelRecipient, DeliveryDispatch, DeliveryResult } from '../../types/index.js';\nimport type { DeliveryExecutor, NotificationsRepository } from '../ports.js';\n\nexport const CHANNEL_NOT_CONFIGURED = 'channel not configured';\nexport const NO_ADDRESS = 'no address';\nexport const NOT_ACCEPTED = 'not accepted';\n\nexport type NotificationDeliveryParams = {\n repo: Pick<NotificationsRepository, 'findDelivery' | 'markSent' | 'recordError' | 'markFailed'>;\n channels: Pick<ChannelRegistry, 'get'>;\n devices: Pick<DevicesService, 'removeDead'>;\n logger?: Logger;\n};\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport class NotificationDelivery implements DeliveryExecutor {\n private readonly repo: NotificationDeliveryParams['repo'];\n private readonly channels: Pick<ChannelRegistry, 'get'>;\n private readonly devices: Pick<DevicesService, 'removeDead'>;\n private readonly logger: Logger;\n\n constructor(params: NotificationDeliveryParams) {\n this.repo = params.repo;\n this.channels = params.channels;\n this.devices = params.devices;\n this.logger = params.logger ?? noopLogger;\n }\n\n async runDelivery(d: DeliveryDispatch, lastAttempt: boolean): Promise<void> {\n const found = await this.repo.findDelivery(d.orgId, d.deliveryId);\n if (!found || found.delivery.status !== 'pending') {\n this.logger.debug('notification delivery skipped', {\n orgId: d.orgId,\n deliveryId: d.deliveryId,\n status: found?.delivery.status ?? null,\n });\n return;\n }\n const { delivery, notification } = found;\n const log = {\n orgId: d.orgId,\n deliveryId: d.deliveryId,\n notificationId: notification.id,\n channel: delivery.channel,\n type: notification.type,\n };\n const channel = this.channels.get(delivery.channel);\n if (!channel) {\n await this.repo.markFailed(d.orgId, d.deliveryId, CHANNEL_NOT_CONFIGURED);\n this.logger.warn('notification delivery failed: channel not configured', log);\n return;\n }\n let to: ChannelRecipient | null;\n let result: DeliveryResult;\n try {\n to = await channel.recipient(d.orgId, notification.userId);\n if (!to) {\n await this.repo.markFailed(d.orgId, d.deliveryId, NO_ADDRESS);\n this.logger.info('notification delivery failed: no address', log);\n return;\n }\n result = await channel.deliver(notification, to);\n } catch (err) {\n const error = messageOf(err);\n if (lastAttempt) {\n await this.repo.markFailed(d.orgId, d.deliveryId, error);\n this.logger.warn('notification delivery failed on its last attempt', { ...log, err });\n return;\n }\n await this.repo.recordError(d.orgId, d.deliveryId, error);\n this.logger.warn('notification delivery attempt failed', { ...log, err });\n throw err;\n }\n if (result.deadDeviceIds.length > 0) {\n await this.devices.removeDead(d.orgId, result.deadDeviceIds);\n }\n if (!result.accepted) {\n const error = result.error ?? NOT_ACCEPTED;\n await this.repo.markFailed(d.orgId, d.deliveryId, error);\n this.logger.info('notification delivery failed: not accepted', { ...log, error });\n return;\n }\n await this.repo.markSent(d.orgId, d.deliveryId, new Date());\n this.logger.info('notification delivered', log);\n }\n}\n"],"mappings":";;AAMA,MAAa,yBAAyB;AACtC,MAAa,aAAa;AAC1B,MAAa,eAAe;AAS5B,SAAS,UAAU,KAAsB;CACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,IAAa,uBAAb,MAA8D;CAC5D;CACA;CACA;CACA;CAEA,YAAY,QAAoC;EAC9C,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW,OAAO;EACvB,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,YAAY,GAAqB,aAAqC;EAC1E,MAAM,QAAQ,MAAM,KAAK,KAAK,aAAa,EAAE,OAAO,EAAE,UAAU;EAChE,IAAI,CAAC,SAAS,MAAM,SAAS,WAAW,WAAW;GACjD,KAAK,OAAO,MAAM,iCAAiC;IACjD,OAAO,EAAE;IACT,YAAY,EAAE;IACd,QAAQ,OAAO,SAAS,UAAU;GACpC,CAAC;GACD;EACF;EACA,MAAM,EAAE,UAAU,iBAAiB;EACnC,MAAM,MAAM;GACV,OAAO,EAAE;GACT,YAAY,EAAE;GACd,gBAAgB,aAAa;GAC7B,SAAS,SAAS;GAClB,MAAM,aAAa;EACrB;EACA,MAAM,UAAU,KAAK,SAAS,IAAI,SAAS,OAAO;EAClD,IAAI,CAAC,SAAS;GACZ,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,sBAAsB;GACxE,KAAK,OAAO,KAAK,wDAAwD,GAAG;GAC5E;EACF;EACA,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,KAAK,MAAM,QAAQ,UAAU,EAAE,OAAO,aAAa,MAAM;GACzD,IAAI,CAAC,IAAI;IACP,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,UAAU;IAC5D,KAAK,OAAO,KAAK,4CAA4C,GAAG;IAChE;GACF;GACA,SAAS,MAAM,QAAQ,QAAQ,cAAc,EAAE;EACjD,SAAS,KAAK;GACZ,MAAM,QAAQ,UAAU,GAAG;GAC3B,IAAI,aAAa;IACf,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK;IACvD,KAAK,OAAO,KAAK,oDAAoD;KAAE,GAAG;KAAK;IAAI,CAAC;IACpF;GACF;GACA,MAAM,KAAK,KAAK,YAAY,EAAE,OAAO,EAAE,YAAY,KAAK;GACxD,KAAK,OAAO,KAAK,wCAAwC;IAAE,GAAG;IAAK;GAAI,CAAC;GACxE,MAAM;EACR;EACA,IAAI,OAAO,cAAc,SAAS,GAChC,MAAM,KAAK,QAAQ,WAAW,EAAE,OAAO,OAAO,aAAa;EAE7D,IAAI,CAAC,OAAO,UAAU;GACpB,MAAM,QAAQ,OAAO,SAAA;GACrB,MAAM,KAAK,KAAK,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK;GACvD,KAAK,OAAO,KAAK,8CAA8C;IAAE,GAAG;IAAK;GAAM,CAAC;GAChF;EACF;EACA,MAAM,KAAK,KAAK,SAAS,EAAE,OAAO,EAAE,4BAAY,IAAI,KAAK,CAAC;EAC1D,KAAK,OAAO,KAAK,0BAA0B,GAAG;CAChD;AACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { noopLogger } from "../../shared/logger.js";
|
|
2
|
-
import { NotFoundError } from "../../shared/errors.js";
|
|
3
1
|
import { genId } from "../../shared/id.js";
|
|
2
|
+
import { NotFoundError } from "../../shared/errors.js";
|
|
3
|
+
import { noopLogger } from "../../shared/logger.js";
|
|
4
4
|
import { resolveChannels } from "../../shared/notification-settings.js";
|
|
5
5
|
//#region src/automations/notifications/processor.ts
|
|
6
6
|
const IN_APP = "inApp";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DomainEvent, Page } from "../types/shared.js";
|
|
2
2
|
import { Event } from "../types/schemas/content.js";
|
|
3
3
|
import { Automation, AutomationKind, AutomationRun, AutomationRunsQuery, AutomationsQuery, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, CreatedWebhook, UpdateAutomationInput, UpdateWebhookInput, Webhook, WebhookSecret } from "../types/schemas/automations.js";
|
|
4
|
-
import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, ScheduledAutomation, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
|
|
4
|
+
import { AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, DeliveryExecutor, ScheduledAutomation, WebhookRequest, WebhookResponse, WebhookSender } from "../types/automations.js";
|
|
5
5
|
import { Delivery, NotificationCategory, NotificationFilter, RecipientList, StoredNotification, SubjectRef } from "../types/notifications.js";
|
|
6
6
|
import "../types/index.js";
|
|
7
7
|
import { Actor } from "../shared/actor.js";
|
|
@@ -184,5 +184,5 @@ interface AutomationsTxScope {
|
|
|
184
184
|
}
|
|
185
185
|
type AutomationsUnitOfWork = UnitOfWork<AutomationsTxScope>;
|
|
186
186
|
//#endregion
|
|
187
|
-
export { type AutomationDispatch, type AutomationEngine, type AutomationExecutor, AutomationRunsRepository, AutomationsRepository, AutomationsService, AutomationsTxScope, AutomationsUnitOfWork, ChatDebounce, type DeliveryDispatch, ListedNotification, NewAutomation, NewAutomationRun, NewDelivery, NewNotification, NotificationCategories, NotificationPage, NotificationReads, NotificationsRepository, NotificationsService, RecipientRule, UnreadCounts, type WebhookRequest, type WebhookResponse, type WebhookSender };
|
|
187
|
+
export { type AutomationDispatch, type AutomationEngine, type AutomationExecutor, AutomationRunsRepository, AutomationsRepository, AutomationsService, AutomationsTxScope, AutomationsUnitOfWork, ChatDebounce, type DeliveryDispatch, type DeliveryExecutor, ListedNotification, NewAutomation, NewAutomationRun, NewDelivery, NewNotification, NotificationCategories, NotificationPage, NotificationReads, NotificationsRepository, NotificationsService, RecipientRule, UnreadCounts, type WebhookRequest, type WebhookResponse, type WebhookSender };
|
|
188
188
|
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DomainEvent, Page } from "../types/shared.js";
|
|
2
2
|
import { Logger } from "../types/ports.js";
|
|
3
3
|
import { Automation, AutomationActionResult, AutomationRun, AutomationRunsQuery, AutomationsQuery, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, CreatedWebhook, UpdateAutomationInput, UpdateWebhookInput, Webhook, WebhookSecret } from "../types/schemas/automations.js";
|
|
4
|
-
import { AutomationDispatch, AutomationEngine, AutomationExecutor,
|
|
4
|
+
import { AutomationDispatch, AutomationEngine, AutomationExecutor, ScheduledAutomation, WebhookSender } from "../types/automations.js";
|
|
5
5
|
import "../types/index.js";
|
|
6
6
|
import { Actor } from "../shared/actor.js";
|
|
7
7
|
import { CredentialStore } from "../shared/ports.js";
|
|
@@ -10,8 +10,6 @@ import { Mailer, MailerLookups } from "../shared/mailer.js";
|
|
|
10
10
|
import "./types.js";
|
|
11
11
|
import { AutomationRunsRepository, AutomationsRepository, AutomationsService, AutomationsUnitOfWork } from "./ports.js";
|
|
12
12
|
import { CommunityMailReads, GrantWriter } from "./community-mail.js";
|
|
13
|
-
import { NotificationDelivery } from "./notifications/delivery.js";
|
|
14
|
-
import { NotificationProcessor } from "./notifications/processor.js";
|
|
15
13
|
import { IntegrationsService } from "../integrations/ports.js";
|
|
16
14
|
import "../integrations/index.js";
|
|
17
15
|
//#region src/automations/service.d.ts
|
|
@@ -30,8 +28,6 @@ type AutomationsServiceParams = {
|
|
|
30
28
|
webhooks: WebhookSender;
|
|
31
29
|
mail: Omit<CommunityMailReads, 'recentMessageRecipients'>;
|
|
32
30
|
grants: GrantWriter;
|
|
33
|
-
delivery: Pick<NotificationDelivery, 'run'>;
|
|
34
|
-
processor: Pick<NotificationProcessor, 'process'>;
|
|
35
31
|
logger?: Logger;
|
|
36
32
|
};
|
|
37
33
|
declare class AutomationsServiceImpl implements AutomationsService, AutomationExecutor {
|
|
@@ -47,8 +43,6 @@ declare class AutomationsServiceImpl implements AutomationsService, AutomationEx
|
|
|
47
43
|
private readonly webhooks;
|
|
48
44
|
private readonly mail;
|
|
49
45
|
private readonly grants;
|
|
50
|
-
private readonly delivery;
|
|
51
|
-
private readonly processor;
|
|
52
46
|
private readonly logger;
|
|
53
47
|
constructor(params: AutomationsServiceParams);
|
|
54
48
|
handle(event: DomainEvent): Promise<void>;
|
|
@@ -60,7 +54,6 @@ declare class AutomationsServiceImpl implements AutomationsService, AutomationEx
|
|
|
60
54
|
/** Best-effort: logs and never rethrows, so an org automation never fails `handle`. */
|
|
61
55
|
private recordFailure;
|
|
62
56
|
runAction(d: AutomationDispatch, index: number): Promise<AutomationActionResult>;
|
|
63
|
-
runDelivery(d: DeliveryDispatch, lastAttempt: boolean): Promise<void>;
|
|
64
57
|
listSchedules(): Promise<ScheduledAutomation[]>;
|
|
65
58
|
fireSchedule(orgId: string, automationId: string, at: Date): Promise<void>;
|
|
66
59
|
finalize(d: AutomationDispatch, results: AutomationActionResult[]): Promise<void>;
|