Skip to content

Commit ee603b0

Browse files
committed
fix(api): create social post endpoint
1 parent 431b8df commit ee603b0

File tree

23 files changed

+290
-151
lines changed

23 files changed

+290
-151
lines changed

compose.reverse-proxy.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@ services:
1919
telegram-bot-api:
2020
image: aiogram/telegram-bot-api:latest
2121
env_file:
22-
- ./services/api/.env.local
22+
- ./services/worker/.env.local
2323
environment:
2424
# TELEGRAM_STAT: 1
2525
TELEGRAM_VERBOSITY: 1
2626
TELEGRAM_LOCAL: 1
2727
TELEGRAM_HTTP_IP_ADDRESS: "[::]"
28-
# VIRTUAL_HOST: telegram.liexp.dev
29-
# VIRTUAL_PORT: 8081
3028
tty: false
3129
stdin_open: true
3230
volumes:

packages/@liexp/backend/src/flows/social-post/fetchSocialPostRelations.flow.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,20 @@ export const fetchSocialPostRelations = <
4141
groups: pipe(
4242
sp.groups,
4343
O.fromNullable,
44-
O.filter((g) => g.length > 0),
4544
O.map((a) => (Schema.is(Schema.Array(UUID))(a) ? a : [])),
45+
O.filter((g) => g.length > 0),
4646
),
4747
keywords: pipe(
4848
sp.keywords,
4949
O.fromNullable,
50-
O.filter((k) => k.length > 0),
5150
O.map((a) => (Schema.is(Schema.Array(UUID))(a) ? a : [])),
51+
O.filter((k) => k.length > 0),
5252
),
5353
media: pipe(
5454
sp.media,
5555
O.fromNullable,
56-
O.filter((m) => m.length > 0),
5756
O.map((a) => (Schema.is(Schema.Array(UUID))(a) ? a : [])),
57+
O.filter((k) => k.length > 0),
5858
),
5959
links: O.none(),
6060
groupsMembers: O.none(),

packages/@liexp/backend/src/flows/social-post/getSocialPostById.flow.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export const getSocialPostById =
4747
content: {
4848
...sp.content,
4949
...relations,
50+
media: relations.media.map((m) => ({
51+
...m,
52+
links: [],
53+
areas: [],
54+
events: [],
55+
})),
5056
},
5157
id,
5258
})),

packages/@liexp/backend/src/io/socialPost.io.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { pipe, fp, flow } from "@liexp/core/lib/fp/index.js";
1+
import { flow, fp, pipe } from "@liexp/core/lib/fp/index.js";
22
import {
33
type _DecodeError,
44
DecodeError,
@@ -88,6 +88,8 @@ const decodeSocialPost = (
8888
ig: socialPost.result?.ig ?? undefined,
8989
},
9090
scheduledAt: socialPost.scheduledAt?.toISOString(),
91+
createdAt: socialPost.createdAt.toISOString(),
92+
updatedAt: socialPost.updatedAt.toISOString(),
9193
})),
9294
E.chain(
9395
flow(

packages/@liexp/backend/src/providers/tg/tg.provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const TGBotProvider = (
9595
),
9696
)
9797
.join(":");
98+
9899
logger.debug.log("tg bot provider %O", {
99100
...opts,
100101
token: encryptedToken,

packages/@liexp/backend/src/utils/data-source.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ export const getDataSource = (
8989
): TE.TaskEither<DBError, DataSource> => {
9090
return TE.tryCatch(async () => {
9191
const dataSource = new DataSource(config);
92-
// if (!dataSource.isInitialized) {
93-
// await dataSource.initialize();
94-
// }
9592
return Promise.resolve(dataSource);
9693
}, toDBError());
9794
};

packages/@liexp/shared/src/io/http/SocialPost.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export const CreateSocialPost = Schema.Struct({
110110
useReply: Schema.Boolean,
111111
media: SocialPostContentMedia,
112112
actors: Schema.Array(Actor.omit("excerpt", "body")),
113-
groups: Schema.Array(Group),
113+
groups: Schema.Array(Group.omit("excerpt", "body")),
114114
keywords: Schema.Array(Keyword.omit("socialPosts")),
115115
platforms: Schema.Record({ key: SocialPlatform, value: Schema.Boolean }),
116116
schedule: Schema.Union(Schema.Number, Schema.Undefined),
@@ -136,6 +136,8 @@ export const SocialPost = Schema.Struct({
136136
status: SocialPostStatus,
137137
result: SocialPostPublishResult,
138138
scheduledAt: Schema.Date,
139+
createdAt: Schema.Date,
140+
updatedAt: Schema.Date,
139141
}).annotations({
140142
title: "SocialPost",
141143
});

packages/@liexp/shared/src/io/http/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as Query from "./Query/index.js";
1818
import * as Queue from "./Queue/index.js";
1919
import { ResourcesNames } from "./ResourcesNames.js";
2020
import * as Setting from "./Setting.js";
21+
import * as SocialPost from "./SocialPost.js";
2122
import * as Stats from "./Stats.js";
2223
import * as Story from "./Story.js";
2324
import * as User from "./User.js";
@@ -44,6 +45,7 @@ export {
4445
Query,
4546
Queue,
4647
ResourcesNames,
48+
SocialPost,
4749
Stats,
4850
Story,
4951
User,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as http from "@liexp/shared/lib/io/http/index.js";
2+
import { Arbitrary } from "effect";
3+
import fc from "fast-check";
4+
import { UUIDArb } from "./common/UUID.arbitrary.js";
5+
6+
export const CreateSocialPostArb = Arbitrary.make(
7+
http.SocialPost.CreateSocialPost.omit("actors", "groups", "keywords"),
8+
).map((post) => ({ ...post, actors: [], groups: [], keywords: [], media: [] }));
9+
10+
export const SocialPostArb = Arbitrary.make(
11+
http.SocialPost.SocialPost.omit("id"),
12+
).map((p) => ({
13+
...p,
14+
id: fc.sample(UUIDArb, 1)[0],
15+
createdAt: new Date(),
16+
updatedAt: new Date(),
17+
deletedAt: undefined,
18+
}));

services/admin-web/src/pages/actors/ActorEdit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { type Option } from "effect/Option";
4141
import * as O from "fp-ts/lib/Option.js";
4242
import { pipe } from "fp-ts/lib/function.js";
4343
import * as React from "react";
44-
import { transformActor } from "./ActorCreate";
44+
import { transformActor } from "./ActorCreate.js";
4545

4646
const EditTitle: React.FC = () => {
4747
const record = useRecordContext();

services/api/.env.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
TZ=UTC
22

33
# DEBUG="-@liexp:*,-@liexp:*:debug" # "-"
4+
# DEBUG="@liexp:*,-@liexp:test-db-manager:*"
45
DEBUG="-"
56

67
NODE_ENV=test
@@ -50,4 +51,4 @@ TG_BOT_BASE_API_URL=http://localhost:9876
5051
IG_USERNAME=invalid
5152
IG_PASSWORD=invalid
5253

53-
OPENAI_URL=http://invalid-openai-url/v1
54+
OPENAI_URL=http://invalid-openai-url/v1

services/api/src/app/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export const makeApp = (ctx: ServerContext): express.Express => {
2525
);
2626

2727
app.use(
28-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
2928
jwt({ secret: ctx.env.JWT_SECRET, algorithms: ["HS256"] }).unless({
3029
path: [
3130
{ url: "/v1/links/submit", method: "POST" },
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { MediaEntity } from "@liexp/backend/lib/entities/Media.entity.js";
2+
import { toMediaEntity } from "@liexp/backend/lib/test/utils/entities/index.js";
3+
import { uuid } from "@liexp/shared/lib/io/http/Common/UUID.js";
4+
import { ImageType } from "@liexp/shared/lib/io/http/Media/MediaType.js";
5+
import {
6+
type CreateSocialPost,
7+
TO_PUBLISH,
8+
} from "@liexp/shared/lib/io/http/SocialPost.js";
9+
import { throwTE } from "@liexp/shared/lib/utils/task.utils.js";
10+
import * as tests from "@liexp/test";
11+
import { MediaArb } from "@liexp/test/lib/arbitrary/Media.arbitrary.js";
12+
import { CreateSocialPostArb } from "@liexp/test/lib/arbitrary/SocialPost.arbitrary.js";
13+
import { differenceInMinutes, parseISO } from "date-fns";
14+
import { describe, beforeAll, test, expect } from "vitest";
15+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
16+
17+
describe("Create Social Post", () => {
18+
let Test: AppTest,
19+
authorizationToken: string,
20+
socialPostData: CreateSocialPost;
21+
22+
beforeAll(async () => {
23+
Test = await GetAppTest();
24+
authorizationToken = `Bearer ${Test.ctx.jwt.signUser({
25+
id: "1",
26+
} as any)()}`;
27+
});
28+
29+
beforeEach(() => {
30+
[socialPostData] = tests.fc
31+
.sample(CreateSocialPostArb, 1)
32+
.map((post) => ({ ...post, schedule: undefined }));
33+
});
34+
35+
test("Should create a social post with scheduledAt", async () => {
36+
const now = new Date();
37+
const type = "events";
38+
const id = uuid();
39+
40+
const response = await Test.req
41+
.post(`/v1/social-posts/${type}/${id}`)
42+
.set("Authorization", authorizationToken)
43+
.send(socialPostData);
44+
45+
expect(response.status).toEqual(201);
46+
47+
const { schedule, content, ...expectedSocialPostData } = socialPostData;
48+
expect(response.body.data).toMatchObject({
49+
...expectedSocialPostData,
50+
status: TO_PUBLISH.literals[0],
51+
});
52+
expect(
53+
differenceInMinutes(parseISO(response.body.data.scheduledAt), now),
54+
).toBe(0);
55+
});
56+
57+
test("Should create a social post to publish on TG", async () => {
58+
const type = "events";
59+
const id = uuid();
60+
61+
const response = await Test.req
62+
.post(`/v1/social-posts/${type}/${id}`)
63+
.set("Authorization", authorizationToken)
64+
.send({
65+
...socialPostData,
66+
platforms: {
67+
IG: false,
68+
TG: true,
69+
},
70+
});
71+
72+
expect(response.status).toEqual(201);
73+
expect(response.body.data.platforms).toMatchObject({
74+
TG: true,
75+
IG: false,
76+
});
77+
});
78+
79+
test("Should create a social post with single media", async () => {
80+
const type = "events";
81+
const id = uuid();
82+
83+
const [media] = tests.fc.sample(MediaArb, 1).map((m) => ({
84+
...m,
85+
type: ImageType.members[0].literals[0],
86+
events: [],
87+
links: [],
88+
keywords: [],
89+
areas: [],
90+
featuredInStories: [],
91+
socialPosts: [],
92+
}));
93+
94+
await throwTE(Test.ctx.db.save(MediaEntity, [media]));
95+
96+
const response = await Test.req
97+
.post(`/v1/social-posts/${type}/${id}`)
98+
.set("Authorization", authorizationToken)
99+
.send({
100+
...socialPostData,
101+
media: [
102+
{
103+
id: media.id,
104+
type: "photo",
105+
media: media.location,
106+
thumbnail: media.thumbnail,
107+
},
108+
],
109+
});
110+
111+
expect(response.status).toEqual(201);
112+
expect(response.body.data).toMatchObject({
113+
media: [
114+
{
115+
id: media.id,
116+
type: "photo",
117+
},
118+
],
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)