@m5kdev/module-social 0.34.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.
@@ -0,0 +1,34 @@
1
+ import { SocialPostInput } from "./social.dto.mjs";
2
+ import { SocialPostResult, SocialProvider } from "./social.types.mjs";
3
+ import { BaseService } from "@m5kdev/backend/base/base.service";
4
+ import { ConnectRepository } from "@m5kdev/backend/modules/connect/connect.repository";
5
+ import { FileService } from "@m5kdev/backend/modules/file/file.service";
6
+ import { RequiredServiceActor } from "@m5kdev/backend/base/base.actor";
7
+ import { ServerResultAsync } from "@m5kdev/backend/base/base.dto";
8
+ import { ConnectService } from "@m5kdev/backend/modules/connect/connect.service";
9
+
10
+ //#region src/social.service.d.ts
11
+ declare class SocialService extends BaseService<{
12
+ connect: ConnectRepository;
13
+ }, {
14
+ connect: ConnectService;
15
+ file: FileService;
16
+ }> {
17
+ private providers;
18
+ constructor(repositories: {
19
+ connect: ConnectRepository;
20
+ }, services: {
21
+ connect: ConnectService;
22
+ file: FileService;
23
+ }, providers: SocialProvider[]);
24
+ getProvider(id: string): SocialProvider | null;
25
+ postToProvider(providerId: string, input: SocialPostInput, {
26
+ actor
27
+ }: {
28
+ actor: RequiredServiceActor<"user">;
29
+ }): ServerResultAsync<SocialPostResult>;
30
+ private ensureFreshConnection;
31
+ }
32
+ //#endregion
33
+ export { SocialService };
34
+ //# sourceMappingURL=social.service.d.mts.map
@@ -0,0 +1,56 @@
1
+ import { BaseService } from "@m5kdev/backend/base/base.service";
2
+ import { err, ok } from "neverthrow";
3
+ //#region src/social.service.ts
4
+ var SocialService = class extends BaseService {
5
+ providers = /* @__PURE__ */ new Map();
6
+ constructor(repositories, services, providers) {
7
+ super(repositories, services);
8
+ this.providers = new Map(providers.map((provider) => [provider.id, provider]));
9
+ }
10
+ getProvider(id) {
11
+ return this.providers.get(id) ?? null;
12
+ }
13
+ async postToProvider(providerId, input, { actor }) {
14
+ const provider = this.getProvider(providerId);
15
+ if (!provider) return this.error("BAD_REQUEST", `Unknown provider: ${providerId}`);
16
+ const connectionResult = await this.repository.connect.list({
17
+ userId: actor.userId,
18
+ providers: [providerId]
19
+ });
20
+ if (connectionResult.isErr()) return this.error("INTERNAL_SERVER_ERROR", "Failed to load connection", { cause: connectionResult.error });
21
+ if (connectionResult.value.length === 0) return this.error("NOT_FOUND", `No connection found for provider: ${providerId}`);
22
+ const activeConnection = connectionResult.value.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0];
23
+ const connection = await this.ensureFreshConnection(activeConnection);
24
+ if (connection.isErr()) return this.error("INTERNAL_SERVER_ERROR", "Failed to refresh connection", { cause: connection.error });
25
+ const payload = {
26
+ text: input.text,
27
+ media: input.media,
28
+ visibility: input.visibility ?? "PUBLIC"
29
+ };
30
+ const accessToken = connection.value.accessToken;
31
+ if (!accessToken) return this.error("INTERNAL_SERVER_ERROR", "Missing access token for connection");
32
+ const result = await this.throwablePromise(() => provider.post({
33
+ deps: { fileService: this.service.file },
34
+ context: {
35
+ userId: actor.userId,
36
+ connection: connection.value,
37
+ accessToken
38
+ },
39
+ payload
40
+ }));
41
+ if (result.isErr()) return err(result.error);
42
+ return ok(result.value);
43
+ }
44
+ async ensureFreshConnection(connection) {
45
+ if (!connection.expiresAt || !connection.refreshToken) return ok(connection);
46
+ const expiresAt = new Date(connection.expiresAt);
47
+ if (Date.now() < expiresAt.getTime() - 60 * 1e3) return ok(connection);
48
+ const refreshed = await this.service.connect.refreshToken(connection.id);
49
+ if (refreshed.isErr()) return this.error("INTERNAL_SERVER_ERROR", "Failed to refresh access token", { cause: refreshed.error });
50
+ return ok(refreshed.value);
51
+ }
52
+ };
53
+ //#endregion
54
+ export { SocialService };
55
+
56
+ //# sourceMappingURL=social.service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social.service.mjs","names":[],"sources":["../../src/social.service.ts"],"sourcesContent":["import type { RequiredServiceActor } from \"@m5kdev/backend/base/base.actor\";\r\nimport type { ServerResultAsync } from \"@m5kdev/backend/base/base.dto\";\r\nimport { BaseService } from \"@m5kdev/backend/base/base.service\";\r\nimport type {\r\n ConnectRepository,\r\n ConnectRow,\r\n} from \"@m5kdev/backend/modules/connect/connect.repository\";\r\nimport type { ConnectService } from \"@m5kdev/backend/modules/connect/connect.service\";\r\nimport type { FileService } from \"@m5kdev/backend/modules/file/file.service\";\r\nimport { err, ok } from \"neverthrow\";\r\nimport type { SocialPostInput } from \"./social.dto\";\r\nimport type { SocialPostPayload, SocialPostResult, SocialProvider } from \"./social.types\";\r\n\r\nexport class SocialService extends BaseService<\r\n {\r\n connect: ConnectRepository;\r\n },\r\n {\r\n connect: ConnectService;\r\n file: FileService;\r\n }\r\n> {\r\n private providers = new Map<string, SocialProvider>();\r\n\r\n constructor(\r\n repositories: { connect: ConnectRepository },\r\n services: { connect: ConnectService; file: FileService },\r\n providers: SocialProvider[]\r\n ) {\r\n super(repositories, services);\r\n this.providers = new Map(providers.map((provider) => [provider.id, provider]));\r\n }\r\n\r\n getProvider(id: string): SocialProvider | null {\r\n return this.providers.get(id) ?? null;\r\n }\r\n\r\n async postToProvider(\r\n providerId: string,\r\n input: SocialPostInput,\r\n { actor }: { actor: RequiredServiceActor<\"user\"> }\r\n ): ServerResultAsync<SocialPostResult> {\r\n const provider = this.getProvider(providerId);\r\n if (!provider) {\r\n return this.error(\"BAD_REQUEST\", `Unknown provider: ${providerId}`);\r\n }\r\n\r\n const connectionResult = await this.repository.connect.list({\r\n userId: actor.userId,\r\n providers: [providerId],\r\n });\r\n\r\n if (connectionResult.isErr()) {\r\n return this.error(\"INTERNAL_SERVER_ERROR\", \"Failed to load connection\", {\r\n cause: connectionResult.error,\r\n });\r\n }\r\n\r\n if (connectionResult.value.length === 0) {\r\n return this.error(\"NOT_FOUND\", `No connection found for provider: ${providerId}`);\r\n }\r\n\r\n const activeConnection = connectionResult.value.sort(\r\n (a, b) => b.createdAt.getTime() - a.createdAt.getTime()\r\n )[0];\r\n\r\n const connection = await this.ensureFreshConnection(activeConnection);\r\n if (connection.isErr()) {\r\n return this.error(\"INTERNAL_SERVER_ERROR\", \"Failed to refresh connection\", {\r\n cause: connection.error,\r\n });\r\n }\r\n\r\n const payload: SocialPostPayload = {\r\n text: input.text,\r\n media: input.media,\r\n visibility: input.visibility ?? \"PUBLIC\",\r\n };\r\n\r\n const accessToken = connection.value.accessToken;\r\n if (!accessToken) {\r\n // the connect callback always stores a token — a missing one is a data-integrity fault\r\n return this.error(\"INTERNAL_SERVER_ERROR\", \"Missing access token for connection\");\r\n }\r\n\r\n const result = await this.throwablePromise(() =>\r\n provider.post({\r\n deps: { fileService: this.service.file },\r\n context: {\r\n userId: actor.userId,\r\n connection: connection.value,\r\n accessToken,\r\n },\r\n payload,\r\n })\r\n );\r\n if (result.isErr()) return err(result.error);\r\n\r\n return ok(result.value);\r\n }\r\n\r\n private async ensureFreshConnection(connection: ConnectRow): ServerResultAsync<ConnectRow> {\r\n if (!connection.expiresAt || !connection.refreshToken) {\r\n return ok(connection);\r\n }\r\n\r\n const expiresAt = new Date(connection.expiresAt);\r\n const bufferMs = 60 * 1000; // Refresh 1 minute before expiry\r\n if (Date.now() < expiresAt.getTime() - bufferMs) {\r\n return ok(connection);\r\n }\r\n\r\n const refreshed = await this.service.connect.refreshToken(connection.id);\r\n if (refreshed.isErr()) {\r\n return this.error(\"INTERNAL_SERVER_ERROR\", \"Failed to refresh access token\", {\r\n cause: refreshed.error,\r\n });\r\n }\r\n\r\n return ok(refreshed.value);\r\n }\r\n}\r\n"],"mappings":";;;AAaA,IAAa,gBAAb,cAAmC,YAQjC;CACA,4BAAoB,IAAI,KAA6B;CAErD,YACE,cACA,UACA,WACA;AACA,QAAM,cAAc,SAAS;AAC7B,OAAK,YAAY,IAAI,IAAI,UAAU,KAAK,aAAa,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC;;CAGhF,YAAY,IAAmC;AAC7C,SAAO,KAAK,UAAU,IAAI,GAAG,IAAI;;CAGnC,MAAM,eACJ,YACA,OACA,EAAE,SACmC;EACrC,MAAM,WAAW,KAAK,YAAY,WAAW;AAC7C,MAAI,CAAC,SACH,QAAO,KAAK,MAAM,eAAe,qBAAqB,aAAa;EAGrE,MAAM,mBAAmB,MAAM,KAAK,WAAW,QAAQ,KAAK;GAC1D,QAAQ,MAAM;GACd,WAAW,CAAC,WAAW;GACxB,CAAC;AAEF,MAAI,iBAAiB,OAAO,CAC1B,QAAO,KAAK,MAAM,yBAAyB,6BAA6B,EACtE,OAAO,iBAAiB,OACzB,CAAC;AAGJ,MAAI,iBAAiB,MAAM,WAAW,EACpC,QAAO,KAAK,MAAM,aAAa,qCAAqC,aAAa;EAGnF,MAAM,mBAAmB,iBAAiB,MAAM,MAC7C,GAAG,MAAM,EAAE,UAAU,SAAS,GAAG,EAAE,UAAU,SAAS,CACxD,CAAC;EAEF,MAAM,aAAa,MAAM,KAAK,sBAAsB,iBAAiB;AACrE,MAAI,WAAW,OAAO,CACpB,QAAO,KAAK,MAAM,yBAAyB,gCAAgC,EACzE,OAAO,WAAW,OACnB,CAAC;EAGJ,MAAM,UAA6B;GACjC,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,YAAY,MAAM,cAAc;GACjC;EAED,MAAM,cAAc,WAAW,MAAM;AACrC,MAAI,CAAC,YAEH,QAAO,KAAK,MAAM,yBAAyB,sCAAsC;EAGnF,MAAM,SAAS,MAAM,KAAK,uBACxB,SAAS,KAAK;GACZ,MAAM,EAAE,aAAa,KAAK,QAAQ,MAAM;GACxC,SAAS;IACP,QAAQ,MAAM;IACd,YAAY,WAAW;IACvB;IACD;GACD;GACD,CAAC,CACH;AACD,MAAI,OAAO,OAAO,CAAE,QAAO,IAAI,OAAO,MAAM;AAE5C,SAAO,GAAG,OAAO,MAAM;;CAGzB,MAAc,sBAAsB,YAAuD;AACzF,MAAI,CAAC,WAAW,aAAa,CAAC,WAAW,aACvC,QAAO,GAAG,WAAW;EAGvB,MAAM,YAAY,IAAI,KAAK,WAAW,UAAU;AAEhD,MAAI,KAAK,KAAK,GAAG,UAAU,SAAS,GADnB,KAAK,IAEpB,QAAO,GAAG,WAAW;EAGvB,MAAM,YAAY,MAAM,KAAK,QAAQ,QAAQ,aAAa,WAAW,GAAG;AACxE,MAAI,UAAU,OAAO,CACnB,QAAO,KAAK,MAAM,yBAAyB,kCAAkC,EAC3E,OAAO,UAAU,OAClB,CAAC;AAGJ,SAAO,GAAG,UAAU,MAAM"}
File without changes
@@ -0,0 +1,40 @@
1
+ import { ConnectRow } from "@m5kdev/backend/modules/connect/connect.repository";
2
+ import { FileService } from "@m5kdev/backend/modules/file/file.service";
3
+
4
+ //#region src/social.types.d.ts
5
+ type SocialMediaType = "image" | "video" | "document";
6
+ interface SocialMediaDescriptor {
7
+ readonly s3Path: string;
8
+ readonly mediaType?: SocialMediaType;
9
+ readonly title?: string;
10
+ readonly description?: string;
11
+ }
12
+ type SocialVisibility = "PUBLIC" | "CONNECTIONS";
13
+ interface SocialPostPayload {
14
+ readonly text: string;
15
+ readonly media?: readonly SocialMediaDescriptor[];
16
+ readonly visibility: SocialVisibility;
17
+ }
18
+ interface SocialProviderContext {
19
+ readonly userId: string;
20
+ readonly connection: ConnectRow;
21
+ readonly accessToken: string;
22
+ }
23
+ interface SocialProviderDeps {
24
+ readonly fileService: FileService;
25
+ }
26
+ interface SocialPostResult {
27
+ readonly shareUrn?: string;
28
+ readonly rawResponse?: unknown;
29
+ }
30
+ interface SocialProvider {
31
+ readonly id: string;
32
+ post(options: {
33
+ deps: SocialProviderDeps;
34
+ context: SocialProviderContext;
35
+ payload: SocialPostPayload;
36
+ }): Promise<SocialPostResult>;
37
+ }
38
+ //#endregion
39
+ export { SocialMediaDescriptor, SocialMediaType, SocialPostPayload, SocialPostResult, SocialProvider, SocialProviderContext, SocialProviderDeps, SocialVisibility };
40
+ //# sourceMappingURL=social.types.d.cts.map
@@ -0,0 +1,40 @@
1
+ import { ConnectRow } from "@m5kdev/backend/modules/connect/connect.repository";
2
+ import { FileService } from "@m5kdev/backend/modules/file/file.service";
3
+
4
+ //#region src/social.types.d.ts
5
+ type SocialMediaType = "image" | "video" | "document";
6
+ interface SocialMediaDescriptor {
7
+ readonly s3Path: string;
8
+ readonly mediaType?: SocialMediaType;
9
+ readonly title?: string;
10
+ readonly description?: string;
11
+ }
12
+ type SocialVisibility = "PUBLIC" | "CONNECTIONS";
13
+ interface SocialPostPayload {
14
+ readonly text: string;
15
+ readonly media?: readonly SocialMediaDescriptor[];
16
+ readonly visibility: SocialVisibility;
17
+ }
18
+ interface SocialProviderContext {
19
+ readonly userId: string;
20
+ readonly connection: ConnectRow;
21
+ readonly accessToken: string;
22
+ }
23
+ interface SocialProviderDeps {
24
+ readonly fileService: FileService;
25
+ }
26
+ interface SocialPostResult {
27
+ readonly shareUrn?: string;
28
+ readonly rawResponse?: unknown;
29
+ }
30
+ interface SocialProvider {
31
+ readonly id: string;
32
+ post(options: {
33
+ deps: SocialProviderDeps;
34
+ context: SocialProviderContext;
35
+ payload: SocialPostPayload;
36
+ }): Promise<SocialPostResult>;
37
+ }
38
+ //#endregion
39
+ export { SocialMediaDescriptor, SocialMediaType, SocialPostPayload, SocialPostResult, SocialProvider, SocialProviderContext, SocialProviderDeps, SocialVisibility };
40
+ //# sourceMappingURL=social.types.d.mts.map
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@m5kdev/module-social",
3
+ "version": "0.34.0",
4
+ "description": "Optional Backend Module for posting to social networks through linked Connection accounts.",
5
+ "license": "GPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/michalkow/m5kdev.git"
9
+ },
10
+ "homepage": "https://github.com/michalkow/m5kdev#readme",
11
+ "bugs": "https://github.com/michalkow/m5kdev/issues",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "@m5kdev/backend": "0.34.0"
17
+ },
18
+ "peerDependencies": {
19
+ "neverthrow": "8.2.0",
20
+ "zod": "4.2.1"
21
+ },
22
+ "devDependencies": {
23
+ "neverthrow": "8.2.0",
24
+ "tsdown": "0.21.7",
25
+ "tslib": "2.8.1",
26
+ "typescript": "6.0.3",
27
+ "zod": "4.2.1",
28
+ "@m5kdev/config": "0.34.0"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/src/social.module.d.mts",
34
+ "default": "./dist/src/social.module.mjs"
35
+ },
36
+ "require": {
37
+ "types": "./dist/src/social.module.d.cts",
38
+ "default": "./dist/src/social.module.cjs"
39
+ }
40
+ },
41
+ "./social.module": {
42
+ "import": {
43
+ "types": "./dist/src/social.module.d.mts",
44
+ "default": "./dist/src/social.module.mjs"
45
+ },
46
+ "require": {
47
+ "types": "./dist/src/social.module.d.cts",
48
+ "default": "./dist/src/social.module.cjs"
49
+ }
50
+ },
51
+ "./social.service": {
52
+ "import": {
53
+ "types": "./dist/src/social.service.d.mts",
54
+ "default": "./dist/src/social.service.mjs"
55
+ },
56
+ "require": {
57
+ "types": "./dist/src/social.service.d.cts",
58
+ "default": "./dist/src/social.service.cjs"
59
+ }
60
+ },
61
+ "./social.linkedin": {
62
+ "import": {
63
+ "types": "./dist/src/social.linkedin.d.mts",
64
+ "default": "./dist/src/social.linkedin.mjs"
65
+ },
66
+ "require": {
67
+ "types": "./dist/src/social.linkedin.d.cts",
68
+ "default": "./dist/src/social.linkedin.cjs"
69
+ }
70
+ },
71
+ "./social.dto": {
72
+ "import": {
73
+ "types": "./dist/src/social.dto.d.mts",
74
+ "default": "./dist/src/social.dto.mjs"
75
+ },
76
+ "require": {
77
+ "types": "./dist/src/social.dto.d.cts",
78
+ "default": "./dist/src/social.dto.cjs"
79
+ }
80
+ },
81
+ "./social.types": {
82
+ "import": {
83
+ "types": "./dist/src/social.types.d.mts",
84
+ "default": "./dist/src/social.types.mjs"
85
+ },
86
+ "require": {
87
+ "types": "./dist/src/social.types.d.cts",
88
+ "default": "./dist/src/social.types.cjs"
89
+ }
90
+ }
91
+ },
92
+ "scripts": {
93
+ "version:patch": "npm version patch",
94
+ "lint": "biome check .",
95
+ "lint:fix": "biome check . --write",
96
+ "build": "tsdown",
97
+ "check-types": "tsc --noEmit"
98
+ }
99
+ }