Leon4gr45 commited on
Commit
a271c58
·
verified ·
1 Parent(s): 57aa51e

Upload folder using huggingface_hub (part 10)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts +114 -0
  2. libraries/nestjs-libraries/src/chat/tools/tool.list.ts +23 -0
  3. libraries/nestjs-libraries/src/chat/tools/upload.from.url.tool.ts +145 -0
  4. libraries/nestjs-libraries/src/chat/tools/video.function.tool.ts +56 -0
  5. libraries/nestjs-libraries/src/chat/validation.schemas.helper.ts +30 -0
  6. libraries/nestjs-libraries/src/database/prisma/admin-stats/admin-stats.repository.ts +157 -0
  7. libraries/nestjs-libraries/src/database/prisma/admin-stats/admin-stats.service.ts +14 -0
  8. libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts +183 -0
  9. libraries/nestjs-libraries/src/database/prisma/agencies/agencies.service.ts +217 -0
  10. libraries/nestjs-libraries/src/database/prisma/announcements/announcements.repository.ts +35 -0
  11. libraries/nestjs-libraries/src/database/prisma/announcements/announcements.service.ts +20 -0
  12. libraries/nestjs-libraries/src/database/prisma/autopost/autopost.repository.ts +107 -0
  13. libraries/nestjs-libraries/src/database/prisma/autopost/autopost.service.ts +371 -0
  14. libraries/nestjs-libraries/src/database/prisma/database.module.ts +105 -0
  15. libraries/nestjs-libraries/src/database/prisma/errors/errors.repository.ts +143 -0
  16. libraries/nestjs-libraries/src/database/prisma/errors/errors.service.ts +21 -0
  17. libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +674 -0
  18. libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +569 -0
  19. libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts +123 -0
  20. libraries/nestjs-libraries/src/database/prisma/media/media.service.ts +157 -0
  21. libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts +117 -0
  22. libraries/nestjs-libraries/src/database/prisma/notifications/notifications.repository.ts +126 -0
  23. libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts +252 -0
  24. libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts +170 -0
  25. libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +422 -0
  26. libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +133 -0
  27. libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +923 -0
  28. libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +1174 -0
  29. libraries/nestjs-libraries/src/database/prisma/prisma.service.ts +39 -0
  30. libraries/nestjs-libraries/src/database/prisma/schema.prisma +970 -0
  31. libraries/nestjs-libraries/src/database/prisma/sets/sets.repository.ts +58 -0
  32. libraries/nestjs-libraries/src/database/prisma/sets/sets.service.ts +24 -0
  33. libraries/nestjs-libraries/src/database/prisma/signatures/signature.repository.ts +55 -0
  34. libraries/nestjs-libraries/src/database/prisma/signatures/signature.service.ts +28 -0
  35. libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +113 -0
  36. libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts +286 -0
  37. libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +264 -0
  38. libraries/nestjs-libraries/src/database/prisma/third-party/third-party.repository.ts +64 -0
  39. libraries/nestjs-libraries/src/database/prisma/third-party/third-party.service.ts +28 -0
  40. libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +177 -0
  41. libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +54 -0
  42. libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.repository.ts +87 -0
  43. libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.service.ts +24 -0
  44. libraries/nestjs-libraries/src/dtos/agencies/create.agency.dto.ts +96 -0
  45. libraries/nestjs-libraries/src/dtos/analytics/stars.list.dto.ts +15 -0
  46. libraries/nestjs-libraries/src/dtos/announcements/announcements.dto.ts +16 -0
  47. libraries/nestjs-libraries/src/dtos/auth/create.org.user.dto.ts +40 -0
  48. libraries/nestjs-libraries/src/dtos/auth/forgot-return.password.dto.ts +28 -0
  49. libraries/nestjs-libraries/src/dtos/auth/forgot.password.dto.ts +8 -0
  50. libraries/nestjs-libraries/src/dtos/auth/login.user.dto.ts +31 -0
libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
2
+ import { createTool } from '@mastra/core/tools';
3
+ import { z } from 'zod';
4
+ import { Injectable } from '@nestjs/common';
5
+ import {
6
+ IntegrationManager,
7
+ socialIntegrationList,
8
+ } from '@gitroom/nestjs-libraries/integrations/integration.manager';
9
+ import { getValidationSchemas } from '@gitroom/nestjs-libraries/chat/validation.schemas.helper';
10
+ import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
11
+
12
+ @Injectable()
13
+ export class IntegrationValidationTool implements AgentToolInterface {
14
+ constructor(private _integrationManager: IntegrationManager) {}
15
+ name = 'integrationSchema';
16
+
17
+ run() {
18
+ return createTool({
19
+ id: 'integrationSchema',
20
+ description: `Everytime we want to schedule a social media post, we need to understand the schema of the integration.
21
+ This tool helps us get the schema of the integration.
22
+ Sometimes we might get a schema back the requires some id, for that, you can get information from 'tools'
23
+ And use the triggerTool function.
24
+ `,
25
+ mcp: {
26
+ annotations: {
27
+ title: 'Get Integration Schema',
28
+ readOnlyHint: true,
29
+ destructiveHint: false,
30
+ idempotentHint: true,
31
+ openWorldHint: false,
32
+ },
33
+ },
34
+ inputSchema: z.object({
35
+ isPremium: z
36
+ .boolean()
37
+ .describe('is this the user premium? if not, set to false'),
38
+ platform: z
39
+ .string()
40
+ .describe(
41
+ `platform identifier (${socialIntegrationList
42
+ .map((p) => p.identifier)
43
+ .join(', ')})`
44
+ ),
45
+ }),
46
+ outputSchema: z.object({
47
+ output: z.object({
48
+ rules: z.string(),
49
+ maxLength: z
50
+ .number()
51
+ .describe('The maximum length of a post / comment'),
52
+ settings: z
53
+ .any()
54
+ .describe('List of settings need to be passed to schedule a post'),
55
+ tools: z
56
+ .array(
57
+ z.object({
58
+ description: z.string().describe('Description of the tool'),
59
+ methodName: z
60
+ .string()
61
+ .describe('Method to call to get the information'),
62
+ dataSchema: z
63
+ .array(
64
+ z.object({
65
+ key: z
66
+ .string()
67
+ .describe('Name of the settings key to pass'),
68
+ description: z
69
+ .string()
70
+ .describe('Description of the setting key'),
71
+ type: z.string(),
72
+ })
73
+ )
74
+ .describe(
75
+ 'This will be passed to schedulePostTool [output:settings]'
76
+ ),
77
+ })
78
+ )
79
+ .describe(
80
+ "Sometimes settings require some id, tags and stuff, if you don't have, trigger the `triggerTool` function from the tools list [output:callable-tools]"
81
+ ),
82
+ }),
83
+ }),
84
+ execute: async (inputData, context) => {
85
+ checkAuth(inputData, context);
86
+ const integration = socialIntegrationList.find(
87
+ (p) => p.identifier === inputData.platform
88
+ )!;
89
+
90
+ if (!integration) {
91
+ return {
92
+ output: { rules: '', maxLength: 0, settings: {}, tools: [] },
93
+ };
94
+ }
95
+
96
+ const maxLength = integration.maxLength(inputData.isPremium);
97
+ const schemas = !integration.dto
98
+ ? false
99
+ : getValidationSchemas()[integration.dto.name];
100
+ const tools = this._integrationManager.getAllTools();
101
+ const rules = this._integrationManager.getAllRulesDescription();
102
+
103
+ return {
104
+ output: {
105
+ rules: rules[integration.identifier],
106
+ maxLength,
107
+ settings: !schemas ? 'No additional settings required' : schemas,
108
+ tools: tools[integration.identifier],
109
+ },
110
+ };
111
+ },
112
+ });
113
+ }
114
+ }
libraries/nestjs-libraries/src/chat/tools/tool.list.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IntegrationValidationTool } from '@gitroom/nestjs-libraries/chat/tools/integration.validation.tool';
2
+ import { IntegrationTriggerTool } from '@gitroom/nestjs-libraries/chat/tools/integration.trigger.tool';
3
+ import { IntegrationSchedulePostTool } from './integration.schedule.post';
4
+ import { GenerateVideoOptionsTool } from '@gitroom/nestjs-libraries/chat/tools/generate.video.options.tool';
5
+ import { VideoFunctionTool } from '@gitroom/nestjs-libraries/chat/tools/video.function.tool';
6
+ import { GenerateVideoTool } from '@gitroom/nestjs-libraries/chat/tools/generate.video.tool';
7
+ import { GenerateImageTool } from '@gitroom/nestjs-libraries/chat/tools/generate.image.tool';
8
+ import { IntegrationListTool } from '@gitroom/nestjs-libraries/chat/tools/integration.list.tool';
9
+ import { GroupListTool } from '@gitroom/nestjs-libraries/chat/tools/group.list.tool';
10
+ import { UploadFromUrlTool } from '@gitroom/nestjs-libraries/chat/tools/upload.from.url.tool';
11
+
12
+ export const toolList = [
13
+ IntegrationListTool,
14
+ GroupListTool,
15
+ IntegrationValidationTool,
16
+ IntegrationTriggerTool,
17
+ IntegrationSchedulePostTool,
18
+ GenerateVideoOptionsTool,
19
+ VideoFunctionTool,
20
+ GenerateVideoTool,
21
+ GenerateImageTool,
22
+ UploadFromUrlTool,
23
+ ];
libraries/nestjs-libraries/src/chat/tools/upload.from.url.tool.ts ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
2
+ import { createTool } from '@mastra/core/tools';
3
+ import { z } from 'zod';
4
+ import { Injectable } from '@nestjs/common';
5
+ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
6
+ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
7
+ import { getMaxSize } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
8
+ import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
9
+ import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher';
10
+ import { Readable } from 'stream';
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const { fromBuffer } = require('file-type');
13
+
14
+ // Same allow-list as the public API /upload-from-url route.
15
+ const ALLOWED_MIME = new Set<string>([
16
+ 'image/jpeg',
17
+ 'image/png',
18
+ 'image/gif',
19
+ 'image/webp',
20
+ 'image/avif',
21
+ 'image/bmp',
22
+ 'image/tiff',
23
+ 'video/mp4',
24
+ ]);
25
+
26
+ @Injectable()
27
+ export class UploadFromUrlTool implements AgentToolInterface {
28
+ private storage = UploadFactory.createStorage();
29
+
30
+ constructor(private _mediaService: MediaService) {}
31
+ name = 'uploadFromUrlTool';
32
+
33
+ run() {
34
+ return createTool({
35
+ id: 'uploadFromUrlTool',
36
+ description: `Upload a remote image or video into the media library from a public URL.
37
+ Use this before scheduling a post when the user provides an external media URL (not already hosted on our domain),
38
+ so the attachment passes the upload-domain validation. Returns the hosted media { id, path } to use as an attachment, or { error } on failure.`,
39
+ mcp: {
40
+ annotations: {
41
+ title: 'Upload Media From URL',
42
+ readOnlyHint: false,
43
+ destructiveHint: false,
44
+ idempotentHint: false,
45
+ openWorldHint: true,
46
+ },
47
+ },
48
+ inputSchema: z.object({
49
+ url: z
50
+ .string()
51
+ .url()
52
+ .describe('The public URL of the image or video to upload'),
53
+ }),
54
+ // Mastra validates a tool's return against this schema, so it must also
55
+ // allow the graceful { error } shape. Fields are optional (rather than
56
+ // wrapping everything in an `output` union) to keep the change minimal:
57
+ // the existing { id, path } success return and the new { error } return
58
+ // both validate without rewriting every return statement.
59
+ outputSchema: z.object({
60
+ id: z.string().optional(),
61
+ path: z.string().optional(),
62
+ error: z.string().optional(),
63
+ }),
64
+ execute: async (inputData, context) => {
65
+ checkAuth(inputData, context);
66
+ try {
67
+ const org = JSON.parse(
68
+ (context?.requestContext as any)?.get('organization') as string
69
+ );
70
+
71
+ const response = await fetch(inputData.url, {
72
+ // @ts-ignore — undici option, not in lib.dom fetch types
73
+ dispatcher: ssrfSafeDispatcher,
74
+ });
75
+
76
+ if (!response.ok) {
77
+ return { error: 'Failed to fetch URL' };
78
+ }
79
+
80
+ // Guard against OOM: bail out before buffering the whole body into
81
+ // memory. Content-Length may be absent or wrong, so we re-check the
82
+ // real size after download too. The type isn't known yet (sniffed
83
+ // below), so the pre-check uses the largest allowed cap (video).
84
+ const maxDownloadSize = getMaxSize('video/mp4');
85
+ const declaredSize = Number(response.headers.get('content-length'));
86
+ if (declaredSize && declaredSize > maxDownloadSize) {
87
+ return {
88
+ error: `File is too large: ${declaredSize} bytes (max ${maxDownloadSize} bytes).`,
89
+ };
90
+ }
91
+
92
+ const buffer = Buffer.from(await response.arrayBuffer());
93
+ const detected = await fromBuffer(buffer);
94
+ if (!detected || !ALLOWED_MIME.has(detected.mime)) {
95
+ return { error: 'Unsupported file type.' };
96
+ }
97
+
98
+ const maxSize = getMaxSize(detected.mime);
99
+ if (buffer.length > maxSize) {
100
+ return {
101
+ error: `File is too large: ${buffer.length} bytes (max ${maxSize} bytes).`,
102
+ };
103
+ }
104
+
105
+ const getFile = await this.storage.uploadFile({
106
+ buffer,
107
+ mimetype: detected.mime,
108
+ size: buffer.length,
109
+ path: '',
110
+ fieldname: '',
111
+ destination: '',
112
+ stream: new Readable(),
113
+ filename: '',
114
+ originalname: `upload.${detected.ext}`,
115
+ encoding: '',
116
+ });
117
+
118
+ return await this._mediaService.saveFile(
119
+ org.id,
120
+ getFile.originalname,
121
+ getFile.path
122
+ );
123
+ } catch (err) {
124
+ // undici's fetch rejects with a generic TypeError('fetch failed')
125
+ // and hides the real reason (DNS, TLS, SSRF block, ...) in
126
+ // err.cause, so surface it for the agent. Error.cause isn't in the
127
+ // es2020 lib typings this repo compiles against, hence the cast.
128
+ const cause =
129
+ err instanceof Error
130
+ ? (err as Error & { cause?: unknown }).cause
131
+ : undefined;
132
+ const causeText =
133
+ cause instanceof Error && cause.message
134
+ ? ` (${cause.message})`
135
+ : '';
136
+ return {
137
+ error: `Failed to upload media from URL: ${
138
+ err instanceof Error ? err.message : 'Unexpected error'
139
+ }${causeText}`,
140
+ };
141
+ }
142
+ },
143
+ });
144
+ }
145
+ }
libraries/nestjs-libraries/src/chat/tools/video.function.tool.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.interface';
2
+ import { createTool } from '@mastra/core/tools';
3
+ import { Injectable } from '@nestjs/common';
4
+ import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
5
+ import z from 'zod';
6
+ import { ModuleRef } from '@nestjs/core';
7
+ import { checkAuth } from '@gitroom/nestjs-libraries/chat/auth.context';
8
+
9
+ @Injectable()
10
+ export class VideoFunctionTool implements AgentToolInterface {
11
+ constructor(
12
+ private _videoManagerService: VideoManager,
13
+ private _moduleRef: ModuleRef
14
+ ) {}
15
+ name = 'videoFunctionTool';
16
+
17
+ run() {
18
+ return createTool({
19
+ id: 'videoFunctionTool',
20
+ description: `Sometimes when we want to generate videos we might need to get some additional information like voice_id, etc`,
21
+ mcp: {
22
+ annotations: {
23
+ title: 'Video Function Helper',
24
+ readOnlyHint: true,
25
+ destructiveHint: false,
26
+ idempotentHint: true,
27
+ openWorldHint: false,
28
+ },
29
+ },
30
+ inputSchema: z.object({
31
+ identifier: z.string(),
32
+ functionName: z.string(),
33
+ }),
34
+ outputSchema: z.object({}).passthrough(),
35
+ execute: async (inputData, context) => {
36
+ checkAuth(inputData, context);
37
+ const videos = this._videoManagerService.getAllVideos();
38
+ const findVideo = videos.find(
39
+ (p) =>
40
+ p.identifier === inputData.identifier &&
41
+ p.tools.some((p) => p.functionName === inputData.functionName)
42
+ );
43
+
44
+ if (!findVideo) {
45
+ throw new Error('Function not found');
46
+ }
47
+
48
+ const func = await this._moduleRef
49
+ // @ts-ignore
50
+ .get(findVideo.target, { strict: false })
51
+ [inputData.functionName]();
52
+ return func;
53
+ },
54
+ });
55
+ }
56
+ }
libraries/nestjs-libraries/src/chat/validation.schemas.helper.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ validationMetadatasToSchemas,
3
+ targetConstructorToSchema,
4
+ } from 'class-validator-jsonschema';
5
+ import { ValidationTypes } from 'class-validator';
6
+ // @ts-ignore
7
+ import { defaultMetadataStorage } from 'class-transformer/cjs/storage';
8
+
9
+ export function getValidationSchemas() {
10
+ return validationMetadatasToSchemas({
11
+ classTransformerMetadataStorage: defaultMetadataStorage,
12
+ additionalConverters: {
13
+ [ValidationTypes.NESTED_VALIDATION]: (meta, options) => {
14
+ if (typeof meta.target === 'function') {
15
+ const typeMeta = options.classTransformerMetadataStorage
16
+ ? options.classTransformerMetadataStorage.findTypeMetadata(
17
+ meta.target,
18
+ meta.propertyName
19
+ )
20
+ : null;
21
+ if (typeMeta) {
22
+ const childType = typeMeta.typeFunction();
23
+ return targetConstructorToSchema(childType, options);
24
+ }
25
+ }
26
+ return {};
27
+ },
28
+ },
29
+ });
30
+ }
libraries/nestjs-libraries/src/database/prisma/admin-stats/admin-stats.repository.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { Prisma } from '@prisma/client';
4
+
5
+ export interface StatsParams {
6
+ from: Date;
7
+ to: Date;
8
+ unknownOnly?: boolean;
9
+ }
10
+
11
+ // Unknown errors are stored as the serialized error payload, e.g.
12
+ // {..."message":"Unknown Error"...}. Matches `message LIKE '%"message":"Unknown Error"%'`.
13
+ const UNKNOWN_ERROR_TOKEN = '"message":"Unknown Error"';
14
+
15
+ interface PerSocial {
16
+ provider: string;
17
+ count: number;
18
+ }
19
+
20
+ export interface StatsResponse {
21
+ from: string;
22
+ to: string;
23
+ errors: { total: number; perSocial: PerSocial[] };
24
+ posts: { total: number; perSocial: PerSocial[] };
25
+ connected: { total: number; perSocial: PerSocial[] };
26
+ }
27
+
28
+ const sortDesc = (list: PerSocial[]) =>
29
+ list.sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider));
30
+
31
+ @Injectable()
32
+ export class AdminStatsRepository {
33
+ constructor(
34
+ private _post: PrismaRepository<'post'>,
35
+ private _integration: PrismaRepository<'integration'>,
36
+ private _errors: PrismaRepository<'errors'>
37
+ ) {}
38
+
39
+ private async errorStats(params: StatsParams) {
40
+ const where: Prisma.ErrorsWhereInput = {
41
+ createdAt: { gte: params.from, lte: params.to },
42
+ ...(params.unknownOnly
43
+ ? { message: { contains: UNKNOWN_ERROR_TOKEN } }
44
+ : {}),
45
+ };
46
+
47
+ const [total, grouped] = await Promise.all([
48
+ this._errors.model.errors.count({ where }),
49
+ this._errors.model.errors.groupBy({
50
+ by: ['platform'],
51
+ where,
52
+ _count: { _all: true },
53
+ }),
54
+ ]);
55
+
56
+ return {
57
+ total,
58
+ perSocial: sortDesc(
59
+ grouped.map((g) => ({
60
+ provider: g.platform,
61
+ count: g._count._all,
62
+ }))
63
+ ),
64
+ };
65
+ }
66
+
67
+ private async postStats(params: StatsParams) {
68
+ // Only count top-level posts (thread children share a parentPostId) so the
69
+ // numbers match a "post published to a channel" rather than every fragment.
70
+ const where: Prisma.PostWhereInput = {
71
+ state: 'PUBLISHED',
72
+ parentPostId: null,
73
+ deletedAt: null,
74
+ publishDate: { gte: params.from, lte: params.to },
75
+ };
76
+
77
+ const [total, grouped] = await Promise.all([
78
+ this._post.model.post.count({ where }),
79
+ this._post.model.post.groupBy({
80
+ by: ['integrationId'],
81
+ where,
82
+ _count: { _all: true },
83
+ }),
84
+ ]);
85
+
86
+ // groupBy can't reach into the integration relation, so resolve the
87
+ // providerIdentifier for the integrations we saw and fold the counts.
88
+ const integrationIds = grouped.map((g) => g.integrationId);
89
+ const integrations = integrationIds.length
90
+ ? await this._integration.model.integration.findMany({
91
+ where: { id: { in: integrationIds } },
92
+ select: { id: true, providerIdentifier: true },
93
+ })
94
+ : [];
95
+ const providerById = new Map(
96
+ integrations.map((i) => [i.id, i.providerIdentifier])
97
+ );
98
+
99
+ const byProvider = new Map<string, number>();
100
+ for (const g of grouped) {
101
+ const provider = providerById.get(g.integrationId) || 'unknown';
102
+ byProvider.set(provider, (byProvider.get(provider) || 0) + g._count._all);
103
+ }
104
+
105
+ return {
106
+ total,
107
+ perSocial: sortDesc(
108
+ [...byProvider.entries()].map(([provider, count]) => ({
109
+ provider,
110
+ count,
111
+ }))
112
+ ),
113
+ };
114
+ }
115
+
116
+ private async connectedStats(params: StatsParams) {
117
+ const where: Prisma.IntegrationWhereInput = {
118
+ deletedAt: null,
119
+ createdAt: { gte: params.from, lte: params.to },
120
+ };
121
+
122
+ const [total, grouped] = await Promise.all([
123
+ this._integration.model.integration.count({ where }),
124
+ this._integration.model.integration.groupBy({
125
+ by: ['providerIdentifier'],
126
+ where,
127
+ _count: { _all: true },
128
+ }),
129
+ ]);
130
+
131
+ return {
132
+ total,
133
+ perSocial: sortDesc(
134
+ grouped.map((g) => ({
135
+ provider: g.providerIdentifier,
136
+ count: g._count._all,
137
+ }))
138
+ ),
139
+ };
140
+ }
141
+
142
+ async getStats(params: StatsParams): Promise<StatsResponse> {
143
+ const [errors, posts, connected] = await Promise.all([
144
+ this.errorStats(params),
145
+ this.postStats(params),
146
+ this.connectedStats(params),
147
+ ]);
148
+
149
+ return {
150
+ from: params.from.toISOString(),
151
+ to: params.to.toISOString(),
152
+ errors,
153
+ posts,
154
+ connected,
155
+ };
156
+ }
157
+ }
libraries/nestjs-libraries/src/database/prisma/admin-stats/admin-stats.service.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import {
3
+ AdminStatsRepository,
4
+ StatsParams,
5
+ } from '@gitroom/nestjs-libraries/database/prisma/admin-stats/admin-stats.repository';
6
+
7
+ @Injectable()
8
+ export class AdminStatsService {
9
+ constructor(private _adminStatsRepository: AdminStatsRepository) {}
10
+
11
+ getStats(params: StatsParams) {
12
+ return this._adminStatsRepository.getStats(params);
13
+ }
14
+ }
libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { User } from '@prisma/client';
4
+ import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.agency.dto';
5
+
6
+ @Injectable()
7
+ export class AgenciesRepository {
8
+ constructor(
9
+ private _socialMediaAgencies: PrismaRepository<'socialMediaAgency'>,
10
+ private _socialMediaAgenciesNiche: PrismaRepository<'socialMediaAgencyNiche'>
11
+ ) {}
12
+
13
+ getAllAgencies() {
14
+ return this._socialMediaAgencies.model.socialMediaAgency.findMany({
15
+ where: {
16
+ deletedAt: null,
17
+ approved: true,
18
+ },
19
+ include: {
20
+ logo: true,
21
+ niches: true,
22
+ },
23
+ orderBy: {
24
+ createdAt: 'desc',
25
+ },
26
+ });
27
+ }
28
+
29
+ getCount() {
30
+ return this._socialMediaAgencies.model.socialMediaAgency.count({
31
+ where: {
32
+ deletedAt: null,
33
+ approved: true,
34
+ },
35
+ });
36
+ }
37
+
38
+ getAllAgenciesSlug() {
39
+ return this._socialMediaAgencies.model.socialMediaAgency.findMany({
40
+ where: {
41
+ deletedAt: null,
42
+ approved: true,
43
+ },
44
+ select: {
45
+ slug: true,
46
+ },
47
+ });
48
+ }
49
+
50
+ approveOrDecline(action: string, id: string) {
51
+ return this._socialMediaAgencies.model.socialMediaAgency.update({
52
+ where: {
53
+ id,
54
+ },
55
+ data: {
56
+ approved: action === 'approve',
57
+ },
58
+ });
59
+ }
60
+
61
+ getAgencyById(id: string) {
62
+ return this._socialMediaAgencies.model.socialMediaAgency.findFirst({
63
+ where: {
64
+ id,
65
+ deletedAt: null,
66
+ approved: true,
67
+ },
68
+ include: {
69
+ logo: true,
70
+ niches: true,
71
+ user: true,
72
+ },
73
+ });
74
+ }
75
+
76
+ getAgencyInformation(agency: string) {
77
+ return this._socialMediaAgencies.model.socialMediaAgency.findFirst({
78
+ where: {
79
+ slug: agency,
80
+ deletedAt: null,
81
+ approved: true,
82
+ },
83
+ include: {
84
+ logo: true,
85
+ niches: true,
86
+ },
87
+ });
88
+ }
89
+
90
+ getAgencyByUser(user: User) {
91
+ return this._socialMediaAgencies.model.socialMediaAgency.findFirst({
92
+ where: {
93
+ userId: user.id,
94
+ deletedAt: null,
95
+ },
96
+ include: {
97
+ logo: true,
98
+ niches: true,
99
+ },
100
+ });
101
+ }
102
+
103
+ async createAgency(user: User, body: CreateAgencyDto) {
104
+ const insertAgency =
105
+ await this._socialMediaAgencies.model.socialMediaAgency.upsert({
106
+ where: {
107
+ userId: user.id,
108
+ },
109
+ update: {
110
+ userId: user.id,
111
+ name: body.name,
112
+ website: body.website,
113
+ facebook: body.facebook,
114
+ instagram: body.instagram,
115
+ twitter: body.twitter,
116
+ linkedIn: body.linkedIn,
117
+ youtube: body.youtube,
118
+ tiktok: body.tiktok,
119
+ logoId: body.logo.id,
120
+ shortDescription: body.shortDescription,
121
+ description: body.description,
122
+ approved: false,
123
+ },
124
+ create: {
125
+ userId: user.id,
126
+ name: body.name,
127
+ website: body.website,
128
+ facebook: body.facebook,
129
+ instagram: body.instagram,
130
+ twitter: body.twitter,
131
+ linkedIn: body.linkedIn,
132
+ youtube: body.youtube,
133
+ tiktok: body.tiktok,
134
+ logoId: body.logo.id,
135
+ shortDescription: body.shortDescription,
136
+ description: body.description,
137
+ slug: body.name.toLowerCase().replace(/ /g, '-'),
138
+ approved: false,
139
+ },
140
+ select: {
141
+ id: true,
142
+ },
143
+ });
144
+
145
+ await this._socialMediaAgenciesNiche.model.socialMediaAgencyNiche.deleteMany(
146
+ {
147
+ where: {
148
+ agencyId: insertAgency.id,
149
+ niche: {
150
+ notIn: body.niches,
151
+ },
152
+ },
153
+ }
154
+ );
155
+
156
+ const currentNiche =
157
+ await this._socialMediaAgenciesNiche.model.socialMediaAgencyNiche.findMany(
158
+ {
159
+ where: {
160
+ agencyId: insertAgency.id,
161
+ },
162
+ select: {
163
+ niche: true,
164
+ },
165
+ }
166
+ );
167
+
168
+ const addNewNiche = body.niches.filter(
169
+ (n) => !currentNiche.some((c) => c.niche === n)
170
+ );
171
+
172
+ await this._socialMediaAgenciesNiche.model.socialMediaAgencyNiche.createMany(
173
+ {
174
+ data: addNewNiche.map((n) => ({
175
+ agencyId: insertAgency.id,
176
+ niche: n,
177
+ })),
178
+ }
179
+ );
180
+
181
+ return insertAgency;
182
+ }
183
+ }
libraries/nestjs-libraries/src/database/prisma/agencies/agencies.service.ts ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
3
+ import { User } from '@prisma/client';
4
+ import { CreateAgencyDto } from '@gitroom/nestjs-libraries/dtos/agencies/create.agency.dto';
5
+ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
6
+
7
+ @Injectable()
8
+ export class AgenciesService {
9
+ constructor(
10
+ private _agenciesRepository: AgenciesRepository,
11
+ private _notificationService: NotificationService
12
+ ) {}
13
+ getAgencyByUser(user: User) {
14
+ return this._agenciesRepository.getAgencyByUser(user);
15
+ }
16
+
17
+ getCount() {
18
+ return this._agenciesRepository.getCount();
19
+ }
20
+
21
+ getAllAgencies() {
22
+ return this._agenciesRepository.getAllAgencies();
23
+ }
24
+
25
+ getAllAgenciesSlug() {
26
+ return this._agenciesRepository.getAllAgenciesSlug();
27
+ }
28
+
29
+ getAgencyInformation(agency: string) {
30
+ return this._agenciesRepository.getAgencyInformation(agency);
31
+ }
32
+
33
+ async approveOrDecline(email: string, action: string, id: string) {
34
+ await this._agenciesRepository.approveOrDecline(action, id);
35
+ const agency = await this._agenciesRepository.getAgencyById(id);
36
+
37
+ if (action === 'approve') {
38
+ await this._notificationService.sendEmail(
39
+ agency?.user?.email!,
40
+ 'Your Agency has been approved and added to Postiz 🚀',
41
+ `
42
+ <html lang="en">
43
+
44
+ <head>
45
+ <meta charset="UTF-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>Your Agency has been approved and added to Postiz 🚀</title>
48
+ </head>
49
+
50
+ <body style="font-family: Arial, sans-serif; margin: 0; padding: 0;">
51
+ Hi there, <br /><br />
52
+ Your agency ${agency?.name} has been added to Postiz!<br />
53
+ You can <a href="https://postiz.com/agencies/${agency?.slug}">check it here</a><br />
54
+ It will appear on the main agency of Postiz in the next 24 hours.<br /><br />
55
+ </body>
56
+ </html>`
57
+ );
58
+
59
+ return;
60
+ }
61
+
62
+ await this._notificationService.sendEmail(
63
+ agency?.user?.email!,
64
+ 'Your Agency has been declined 😔',
65
+ `
66
+ <html lang="en">
67
+
68
+ <head>
69
+ <meta charset="UTF-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
71
+ <title>Your Agency has been declined</title>
72
+ </head>
73
+
74
+ <body style="font-family: Arial, sans-serif; margin: 0; padding: 0;">
75
+ Hi there, <br /><br />
76
+ Your agency ${agency?.name} has been declined to Postiz!<br />
77
+ If you think we have made a mistake, please reply to this email and let us know
78
+ </body>
79
+ </html>`
80
+ );
81
+
82
+ return;
83
+ }
84
+
85
+ async createAgency(user: User, body: CreateAgencyDto) {
86
+ const agency = await this._agenciesRepository.createAgency(user, body);
87
+ await this._notificationService.sendEmail(
88
+ 'nevo@postiz.com',
89
+ 'New agency created',
90
+ `
91
+ <html lang="en">
92
+
93
+ <head>
94
+ <meta charset="UTF-8">
95
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
96
+ <title>Email Template</title>
97
+ </head>
98
+
99
+ <body style="font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4;">
100
+ <table align="center" cellpadding="0" cellspacing="0" style="max-width: 600px; width: 100%; background-color: #ffffff; margin-top: 20px; border: 1px solid #ddd;">
101
+ <tr>
102
+ <td style="padding: 0 20px 20px 20px; text-align: center;">
103
+ <!-- Website -->
104
+ <a href="${
105
+ body.website
106
+ }" style="text-decoration: none; color: #007bff;">${
107
+ body.website
108
+ }</a>
109
+ </td>
110
+ </tr>
111
+ <tr>
112
+ <td style="padding: 20px; text-align: center;">
113
+ <!-- Social Media Links -->
114
+ <p style="margin: 10px 0; font-size: 16px;">
115
+ Social Medias:
116
+ <a href="${
117
+ body.facebook
118
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
119
+ body.facebook
120
+ }</a><br />
121
+ <a href="${
122
+ body.instagram
123
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
124
+ body.instagram
125
+ }</a><br />
126
+ <a href="${
127
+ body.twitter
128
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
129
+ body.twitter
130
+ }</a><br />
131
+ <a href="${
132
+ body.linkedIn
133
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
134
+ body.linkedIn
135
+ }</a><br />
136
+ <a href="${
137
+ body.youtube
138
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
139
+ body.youtube
140
+ }</a><br />
141
+ <a href="${
142
+ body.tiktok
143
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">${
144
+ body.tiktok
145
+ }</a>
146
+ </p>
147
+ </td>
148
+ </tr>
149
+ <tr>
150
+ <td style="padding: 20px;">
151
+ <!-- Short Description -->
152
+ <h2 style="text-align: center; color: #333;">Logo</h2>
153
+ <p style="text-align: center; color: #555; font-size: 16px;">
154
+ <img src="${body.logo.path}" width="60" height="60" />
155
+ </p>
156
+ </td>
157
+ </tr>
158
+ <tr>
159
+ <td style="padding: 20px;">
160
+ <!-- Short Description -->
161
+ <h2 style="text-align: center; color: #333;">Name</h2>
162
+ <p style="text-align: center; color: #555; font-size: 16px;">${
163
+ body.name
164
+ }</p>
165
+ </td>
166
+ </tr>
167
+ <tr>
168
+ <td style="padding: 20px;">
169
+ <!-- Short Description -->
170
+ <h2 style="text-align: center; color: #333;">Short Description</h2>
171
+ <p style="text-align: center; color: #555; font-size: 16px;">${
172
+ body.shortDescription
173
+ }</p>
174
+ </td>
175
+ </tr>
176
+ <tr>
177
+ <td style="padding: 20px;">
178
+ <!-- Description -->
179
+ <h2 style="text-align: center; color: #333;">Description</h2>
180
+ <p style="text-align: center; color: #555; font-size: 16px;">${
181
+ body.description
182
+ }</p>
183
+ </td>
184
+ </tr>
185
+ <tr>
186
+ <td style="padding: 20px;">
187
+ <!-- Niches -->
188
+ <h2 style="text-align: center; color: #333;">Niches</h2>
189
+ <p style="text-align: center; color: #555; font-size: 16px;">${body.niches.join(
190
+ ','
191
+ )}</p>
192
+ </td>
193
+ </tr>
194
+ <tr>
195
+ <td style="padding: 20px; text-align: center; background-color: #000;">
196
+ <a href="https://postiz.com/agencies/action/approve/${
197
+ agency.id
198
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">To approve click here</a><br /><br /><br />
199
+ <a href="https://postiz.com/agencies/action/decline/${
200
+ agency.id
201
+ }" style="margin: 0 10px; text-decoration: none; color: #007bff;">To decline click here</a><br /><br /><br />
202
+ </td>
203
+ </tr>
204
+ <tr>
205
+ <td style="padding: 20px; text-align: center; background-color: #f4f4f4;">
206
+ <p style="color: #777; font-size: 14px;">&copy; 2024 Your Gitroom Limited All rights reserved.</p>
207
+ </td>
208
+ </tr>
209
+ </table>
210
+ </body>
211
+
212
+ </html>
213
+ `
214
+ );
215
+ return agency;
216
+ }
217
+ }
libraries/nestjs-libraries/src/database/prisma/announcements/announcements.repository.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { AnnouncementDto } from '@gitroom/nestjs-libraries/dtos/announcements/announcements.dto';
4
+ import { AnnouncementColor } from '@prisma/client';
5
+
6
+ @Injectable()
7
+ export class AnnouncementsRepository {
8
+ constructor(private _announcements: PrismaRepository<'announcement'>) {}
9
+
10
+ getAnnouncements() {
11
+ return this._announcements.model.announcement.findMany({
12
+ orderBy: {
13
+ createdAt: 'desc',
14
+ },
15
+ });
16
+ }
17
+
18
+ createAnnouncement(body: AnnouncementDto) {
19
+ return this._announcements.model.announcement.create({
20
+ data: {
21
+ title: body.title,
22
+ description: body.description,
23
+ color: (body.color as AnnouncementColor) || AnnouncementColor.INFO,
24
+ },
25
+ });
26
+ }
27
+
28
+ deleteAnnouncement(id: string) {
29
+ return this._announcements.model.announcement.delete({
30
+ where: {
31
+ id,
32
+ },
33
+ });
34
+ }
35
+ }
libraries/nestjs-libraries/src/database/prisma/announcements/announcements.service.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AnnouncementsRepository } from '@gitroom/nestjs-libraries/database/prisma/announcements/announcements.repository';
3
+ import { AnnouncementDto } from '@gitroom/nestjs-libraries/dtos/announcements/announcements.dto';
4
+
5
+ @Injectable()
6
+ export class AnnouncementsService {
7
+ constructor(private _announcementsRepository: AnnouncementsRepository) {}
8
+
9
+ getAnnouncements() {
10
+ return this._announcementsRepository.getAnnouncements();
11
+ }
12
+
13
+ createAnnouncement(body: AnnouncementDto) {
14
+ return this._announcementsRepository.createAnnouncement(body);
15
+ }
16
+
17
+ deleteAnnouncement(id: string) {
18
+ return this._announcementsRepository.deleteAnnouncement(id);
19
+ }
20
+ }
libraries/nestjs-libraries/src/database/prisma/autopost/autopost.repository.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
5
+
6
+ @Injectable()
7
+ export class AutopostRepository {
8
+ constructor(private _autoPost: PrismaRepository<'autoPost'>) {}
9
+
10
+ getTotal(orgId: string) {
11
+ return this._autoPost.model.autoPost.count({
12
+ where: {
13
+ organizationId: orgId,
14
+ deletedAt: null,
15
+ },
16
+ });
17
+ }
18
+
19
+ getAutoposts(orgId: string) {
20
+ return this._autoPost.model.autoPost.findMany({
21
+ where: {
22
+ organizationId: orgId,
23
+ deletedAt: null,
24
+ },
25
+ });
26
+ }
27
+
28
+ deleteAutopost(orgId: string, id: string) {
29
+ return this._autoPost.model.autoPost.update({
30
+ where: {
31
+ id,
32
+ organizationId: orgId,
33
+ },
34
+ data: {
35
+ deletedAt: new Date(),
36
+ },
37
+ });
38
+ }
39
+
40
+ getAutopost(id: string) {
41
+ return this._autoPost.model.autoPost.findUnique({
42
+ where: {
43
+ id,
44
+ deletedAt: null,
45
+ },
46
+ });
47
+ }
48
+
49
+ updateUrl(id: string, url: string) {
50
+ return this._autoPost.model.autoPost.update({
51
+ where: {
52
+ id,
53
+ },
54
+ data: {
55
+ lastUrl: url,
56
+ },
57
+ });
58
+ }
59
+
60
+ changeActive(orgId: string, id: string, active: boolean) {
61
+ return this._autoPost.model.autoPost.update({
62
+ where: {
63
+ id,
64
+ organizationId: orgId,
65
+ },
66
+ data: {
67
+ active,
68
+ },
69
+ });
70
+ }
71
+
72
+ async createAutopost(orgId: string, body: AutopostDto, id?: string) {
73
+ const { id: newId, active } = await this._autoPost.model.autoPost.upsert({
74
+ where: {
75
+ id: id || uuidv4(),
76
+ organizationId: orgId,
77
+ },
78
+ create: {
79
+ organizationId: orgId,
80
+ url: body.url,
81
+ title: body.title,
82
+ integrations: JSON.stringify(body.integrations),
83
+ active: body.active,
84
+ content: body.content,
85
+ generateContent: body.generateContent,
86
+ addPicture: body.addPicture,
87
+ syncLast: body.syncLast,
88
+ onSlot: body.onSlot,
89
+ lastUrl: body.lastUrl,
90
+ },
91
+ update: {
92
+ url: body.url,
93
+ title: body.title,
94
+ integrations: JSON.stringify(body.integrations),
95
+ active: body.active,
96
+ content: body.content,
97
+ generateContent: body.generateContent,
98
+ addPicture: body.addPicture,
99
+ syncLast: body.syncLast,
100
+ onSlot: body.onSlot,
101
+ lastUrl: body.lastUrl,
102
+ },
103
+ });
104
+
105
+ return { id: newId, active };
106
+ }
107
+ }
libraries/nestjs-libraries/src/database/prisma/autopost/autopost.service.ts ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
3
+ import { AutopostDto } from '@gitroom/nestjs-libraries/dtos/autopost/autopost.dto';
4
+ import dayjs from 'dayjs';
5
+ import { END, START, StateGraph } from '@langchain/langgraph';
6
+ import { AutoPost, Integration } from '@prisma/client';
7
+ import { BaseMessage } from '@langchain/core/messages';
8
+ import striptags from 'striptags';
9
+ import { ChatOpenAI, DallEAPIWrapper } from '@langchain/openai';
10
+ import { JSDOM } from 'jsdom';
11
+ import { z } from 'zod';
12
+ import { ChatPromptTemplate } from '@langchain/core/prompts';
13
+ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
14
+ import Parser from 'rss-parser';
15
+ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
16
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
17
+ import { TemporalService } from 'nestjs-temporal-core';
18
+ import { TypedSearchAttributes } from '@temporalio/common';
19
+ import {
20
+ organizationId,
21
+ } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
22
+ const parser = new Parser();
23
+
24
+ interface WorkflowChannelsState {
25
+ messages: BaseMessage[];
26
+ integrations: Integration[];
27
+ body: AutoPost;
28
+ description: string;
29
+ image: string;
30
+ id: string;
31
+ load: {
32
+ date: string;
33
+ url: string;
34
+ description: string;
35
+ };
36
+ }
37
+
38
+ const model = new ChatOpenAI({
39
+ apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
40
+ model: 'gpt-4.1',
41
+ temperature: 0.7,
42
+ });
43
+
44
+ const dalle = new DallEAPIWrapper({
45
+ apiKey: process.env.OPENAI_API_KEY || 'sk-proj-',
46
+ model: 'chatgpt-image-latest',
47
+ });
48
+
49
+ const generateContent = z.object({
50
+ socialMediaPostContent: z
51
+ .string()
52
+ .describe('Content for social media posts max 120 chars'),
53
+ });
54
+
55
+ const dallePrompt = z.object({
56
+ generatedTextToBeSentToDallE: z
57
+ .string()
58
+ .describe('Generated prompt from description to be sent to DallE'),
59
+ });
60
+
61
+ @Injectable()
62
+ export class AutopostService {
63
+ constructor(
64
+ private _autopostsRepository: AutopostRepository,
65
+ private _temporalService: TemporalService,
66
+ private _integrationService: IntegrationService,
67
+ private _postsService: PostsService
68
+ ) {}
69
+
70
+ async stopAll(org: string) {
71
+ const getAll = (await this.getAutoposts(org)).filter((f) => f.active);
72
+ for (const autopost of getAll) {
73
+ await this.changeActive(org, autopost.id, false);
74
+ }
75
+ }
76
+
77
+ getAutoposts(orgId: string) {
78
+ return this._autopostsRepository.getAutoposts(orgId);
79
+ }
80
+
81
+ async createAutopost(orgId: string, body: AutopostDto, id?: string) {
82
+ const data = await this._autopostsRepository.createAutopost(
83
+ orgId,
84
+ body,
85
+ id
86
+ );
87
+
88
+ await this.processCron(body.active, orgId, data.id);
89
+
90
+ return data;
91
+ }
92
+
93
+ async changeActive(orgId: string, id: string, active: boolean) {
94
+ const data = await this._autopostsRepository.changeActive(
95
+ orgId,
96
+ id,
97
+ active
98
+ );
99
+ await this.processCron(active, orgId, id);
100
+ return data;
101
+ }
102
+
103
+ async processCron(active: boolean, orgId: string, id: string) {
104
+ if (active) {
105
+ try {
106
+ return this._temporalService.client
107
+ .getRawClient()
108
+ ?.workflow.start('autoPostWorkflow', {
109
+ workflowId: `autopost-${id}`,
110
+ taskQueue: 'main',
111
+ args: [{ id, immediately: true }],
112
+ typedSearchAttributes: new TypedSearchAttributes([
113
+ {
114
+ key: organizationId,
115
+ value: orgId,
116
+ },
117
+ ]),
118
+ });
119
+ } catch (err) {}
120
+ }
121
+
122
+ try {
123
+ return await this._temporalService.terminateWorkflow(`autopost-${id}`);
124
+ } catch (err) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ async deleteAutopost(orgId: string, id: string) {
130
+ const data = await this._autopostsRepository.deleteAutopost(orgId, id);
131
+ await this.processCron(false, orgId, id);
132
+ return data;
133
+ }
134
+
135
+ async loadXML(url: string) {
136
+ try {
137
+ const { items } = await parser.parseURL(url);
138
+ const findLast = items.reduce(
139
+ (all: any, current: any) => {
140
+ if (dayjs(current.pubDate).isAfter(all.pubDate)) {
141
+ return current;
142
+ }
143
+ return all;
144
+ },
145
+ { pubDate: dayjs().subtract(100, 'years') }
146
+ );
147
+
148
+ return {
149
+ success: true,
150
+ date: findLast.pubDate,
151
+ url: findLast.link,
152
+ description: striptags(
153
+ findLast?.['content:encoded'] ||
154
+ findLast?.content ||
155
+ findLast?.description ||
156
+ ''
157
+ )
158
+ .replace(/\n/g, ' ')
159
+ .trim(),
160
+ };
161
+ } catch (err) {
162
+ /** sent **/
163
+ }
164
+
165
+ return { success: false };
166
+ }
167
+
168
+ static state = () =>
169
+ new StateGraph<WorkflowChannelsState>({
170
+ channels: {
171
+ messages: {
172
+ reducer: (currentState, updateValue) =>
173
+ currentState.concat(updateValue),
174
+ default: () => [],
175
+ },
176
+ body: null,
177
+ description: null,
178
+ load: null,
179
+ image: null,
180
+ integrations: null,
181
+ id: null,
182
+ },
183
+ });
184
+
185
+ async loadUrl(url: string) {
186
+ try {
187
+ const loadDom = new JSDOM(await (await fetch(url)).text());
188
+ loadDom.window.document
189
+ .querySelectorAll('script')
190
+ .forEach((s) => s.remove());
191
+ loadDom.window.document
192
+ .querySelectorAll('style')
193
+ .forEach((s) => s.remove());
194
+ // remove all html, script and styles
195
+ return striptags(loadDom.window.document.body.innerHTML);
196
+ } catch (err) {
197
+ return '';
198
+ }
199
+ }
200
+
201
+ async generateDescription(state: WorkflowChannelsState) {
202
+ if (!state.body.generateContent) {
203
+ return {
204
+ ...state,
205
+ description: state.body.content,
206
+ };
207
+ }
208
+
209
+ const description =
210
+ state.load.description || (await this.loadUrl(state.load.url));
211
+ if (!description) {
212
+ return {
213
+ ...state,
214
+ description: '',
215
+ };
216
+ }
217
+
218
+ const structuredOutput = model.withStructuredOutput(generateContent);
219
+ const { socialMediaPostContent } = await ChatPromptTemplate.fromTemplate(
220
+ `
221
+ You are an assistant that gets raw 'description' of a content and generate a social media post content.
222
+ Rules:
223
+ - Maximum 100 chars
224
+ - Try to make it a short as possible to fit any social media
225
+ - Add line breaks between sentences (\\n)
226
+ - Don't add hashtags
227
+ - Add emojis when needed
228
+
229
+ 'description':
230
+ {content}
231
+ `
232
+ )
233
+ .pipe(structuredOutput)
234
+ .invoke({
235
+ content: description,
236
+ });
237
+
238
+ return {
239
+ ...state,
240
+ description: socialMediaPostContent,
241
+ };
242
+ }
243
+
244
+ async generatePicture(state: WorkflowChannelsState) {
245
+ const structuredOutput = model.withStructuredOutput(dallePrompt);
246
+ const { generatedTextToBeSentToDallE } =
247
+ await ChatPromptTemplate.fromTemplate(
248
+ `
249
+ You are an assistant that gets description and generate a prompt that will be sent to DallE to generate pictures.
250
+
251
+ content:
252
+ {content}
253
+ `
254
+ )
255
+ .pipe(structuredOutput)
256
+ .invoke({
257
+ content: state.load.description || state.description,
258
+ });
259
+
260
+ const image = await dalle.invoke(generatedTextToBeSentToDallE);
261
+
262
+ return { ...state, image };
263
+ }
264
+
265
+ async schedulePost(state: WorkflowChannelsState) {
266
+ const nextTime = await this._postsService.findFreeDateTime(
267
+ state.integrations[0].organizationId
268
+ );
269
+
270
+ await this._postsService.createPost(state.integrations[0].organizationId, {
271
+ date: nextTime + 'Z',
272
+ order: makeId(10),
273
+ shortLink: false,
274
+ type: 'draft',
275
+ tags: [],
276
+ posts: state.integrations.map((i) => ({
277
+ settings: {
278
+ __type: i.providerIdentifier as any,
279
+ title: '',
280
+ tags: [],
281
+ subreddit: [],
282
+ },
283
+ group: makeId(10),
284
+ integration: { id: i.id },
285
+ value: [
286
+ {
287
+ id: makeId(10),
288
+ delay: 0,
289
+ content:
290
+ state.description.replace(/\n/g, '\n\n') +
291
+ '\n\n' +
292
+ state.load.url,
293
+ image: !state.image
294
+ ? []
295
+ : [
296
+ {
297
+ id: makeId(10),
298
+ name: makeId(10),
299
+ path: state.image,
300
+ organizationId: state.integrations[0].organizationId,
301
+ },
302
+ ],
303
+ },
304
+ ],
305
+ })),
306
+ }, 'AUTOPOST');
307
+ }
308
+
309
+ async updateUrl(state: WorkflowChannelsState) {
310
+ await this._autopostsRepository.updateUrl(state.id, state.load.url);
311
+ }
312
+
313
+ async startAutopost(id: string) {
314
+ const getPost = await this._autopostsRepository.getAutopost(id);
315
+ if (!getPost || !getPost.active) {
316
+ return;
317
+ }
318
+
319
+ const load = await this.loadXML(getPost.url);
320
+ if (!load.success || load.url === getPost.lastUrl) {
321
+ return;
322
+ }
323
+
324
+ const integrations = await this._integrationService.getIntegrationsList(
325
+ getPost.organizationId
326
+ );
327
+
328
+ const parseIntegrations = JSON.parse(getPost.integrations || '[]') || [];
329
+ const neededIntegrations = integrations.filter((i) =>
330
+ parseIntegrations.some((ii: any) => ii.id === i.id)
331
+ );
332
+
333
+ const integrationsToSend =
334
+ parseIntegrations.length === 0 ? integrations : neededIntegrations;
335
+ if (integrationsToSend.length === 0) {
336
+ return;
337
+ }
338
+
339
+ const state = AutopostService.state();
340
+ const workflow = state
341
+ .addNode('generate-description', this.generateDescription.bind(this))
342
+ .addNode('generate-picture', this.generatePicture.bind(this))
343
+ .addNode('schedule-post', this.schedulePost.bind(this))
344
+ .addNode('update-url', this.updateUrl.bind(this))
345
+ .addEdge(START, 'generate-description')
346
+ .addConditionalEdges(
347
+ 'generate-description',
348
+ (state: WorkflowChannelsState) => {
349
+ if (!state.description) {
350
+ return 'schedule-post';
351
+ }
352
+ if (state.body.addPicture) {
353
+ return 'generate-picture';
354
+ }
355
+ return 'schedule-post';
356
+ }
357
+ )
358
+ .addEdge('generate-picture', 'schedule-post')
359
+ .addEdge('schedule-post', 'update-url')
360
+ .addEdge('update-url', END);
361
+
362
+ const app = workflow.compile();
363
+ await app.invoke({
364
+ messages: [],
365
+ id,
366
+ body: getPost,
367
+ load,
368
+ integrations: integrationsToSend,
369
+ });
370
+ }
371
+ }
libraries/nestjs-libraries/src/database/prisma/database.module.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { PrismaRepository, PrismaService, PrismaTransaction } from './prisma.service';
3
+ import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
4
+ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
5
+ import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
6
+ import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
7
+ import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
8
+ import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository';
9
+ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
10
+ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
11
+ import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
12
+ import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
13
+ import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
14
+ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
15
+ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
16
+ import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository';
17
+ import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository';
18
+ import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
19
+ import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
20
+ import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
21
+ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
22
+ import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
23
+ import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
24
+ import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
25
+ import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
26
+ import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.repository';
27
+ import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
28
+ import { SignatureRepository } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.repository';
29
+ import { SignatureService } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.service';
30
+ import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
31
+ import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
32
+ import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.service';
33
+ import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository';
34
+ import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.repository';
35
+ import { ThirdPartyService } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.service';
36
+ import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
37
+ import { FalService } from '@gitroom/nestjs-libraries/openai/fal.service';
38
+ import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
39
+ import { OAuthRepository } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.repository';
40
+ import { OAuthService } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.service';
41
+ import { AnnouncementsRepository } from '@gitroom/nestjs-libraries/database/prisma/announcements/announcements.repository';
42
+ import { AnnouncementsService } from '@gitroom/nestjs-libraries/database/prisma/announcements/announcements.service';
43
+ import { ErrorsRepository } from '@gitroom/nestjs-libraries/database/prisma/errors/errors.repository';
44
+ import { ErrorsService } from '@gitroom/nestjs-libraries/database/prisma/errors/errors.service';
45
+ import { AdminStatsRepository } from '@gitroom/nestjs-libraries/database/prisma/admin-stats/admin-stats.repository';
46
+ import { AdminStatsService } from '@gitroom/nestjs-libraries/database/prisma/admin-stats/admin-stats.service';
47
+
48
+ @Global()
49
+ @Module({
50
+ imports: [],
51
+ controllers: [],
52
+ providers: [
53
+ PrismaService,
54
+ PrismaRepository,
55
+ PrismaTransaction,
56
+ UsersService,
57
+ UsersRepository,
58
+ OrganizationService,
59
+ OrganizationRepository,
60
+ SubscriptionService,
61
+ SubscriptionRepository,
62
+ NotificationService,
63
+ NotificationsRepository,
64
+ WebhooksRepository,
65
+ WebhooksService,
66
+ IntegrationService,
67
+ IntegrationRepository,
68
+ PostsService,
69
+ PostsRepository,
70
+ StripeService,
71
+ SignatureRepository,
72
+ AutopostRepository,
73
+ AutopostService,
74
+ SignatureService,
75
+ MediaService,
76
+ MediaRepository,
77
+ AgenciesService,
78
+ AgenciesRepository,
79
+ IntegrationManager,
80
+ RefreshIntegrationService,
81
+ ExtractContentService,
82
+ OpenaiService,
83
+ FalService,
84
+ EmailService,
85
+ TrackService,
86
+ ShortLinkService,
87
+ SetsService,
88
+ SetsRepository,
89
+ ThirdPartyRepository,
90
+ ThirdPartyService,
91
+ OAuthRepository,
92
+ OAuthService,
93
+ VideoManager,
94
+ AnnouncementsRepository,
95
+ AnnouncementsService,
96
+ ErrorsRepository,
97
+ ErrorsService,
98
+ AdminStatsRepository,
99
+ AdminStatsService,
100
+ ],
101
+ get exports() {
102
+ return this.providers;
103
+ },
104
+ })
105
+ export class DatabaseModule {}
libraries/nestjs-libraries/src/database/prisma/errors/errors.repository.ts ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+
4
+ const UNKNOWN_TOKEN = 'An unknown error occurred';
5
+
6
+ interface ListErrorsParams {
7
+ page?: number;
8
+ limit?: number;
9
+ platform?: string;
10
+ email?: string;
11
+ unknownFirst?: boolean;
12
+ }
13
+
14
+ @Injectable()
15
+ export class ErrorsRepository {
16
+ constructor(private _errors: PrismaRepository<'errors'>) {}
17
+
18
+ private buildWhere(params: ListErrorsParams) {
19
+ const where: any = {};
20
+ if (params.platform) {
21
+ where.platform = params.platform;
22
+ }
23
+ if (params.email) {
24
+ where.organization = {
25
+ users: {
26
+ some: {
27
+ user: {
28
+ email: { contains: params.email, mode: 'insensitive' },
29
+ },
30
+ },
31
+ },
32
+ };
33
+ }
34
+ return where;
35
+ }
36
+
37
+ private get include() {
38
+ return {
39
+ organization: {
40
+ select: {
41
+ id: true,
42
+ name: true,
43
+ users: {
44
+ select: {
45
+ user: { select: { id: true, email: true, name: true } },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ post: { select: { id: true, content: true } },
51
+ } as const;
52
+ }
53
+
54
+ async listPlatforms() {
55
+ const rows = await this._errors.model.errors.findMany({
56
+ distinct: ['platform'],
57
+ select: { platform: true },
58
+ orderBy: { platform: 'asc' },
59
+ });
60
+ return rows.map((r) => r.platform);
61
+ }
62
+
63
+ async listErrors(params: ListErrorsParams) {
64
+ const page = Math.max(0, params.page || 0);
65
+ const limit = Math.min(Math.max(1, params.limit || 20), 100);
66
+ const skip = page * limit;
67
+ const where = this.buildWhere(params);
68
+ const include = this.include;
69
+
70
+ if (!params.unknownFirst) {
71
+ const [items, total] = await Promise.all([
72
+ this._errors.model.errors.findMany({
73
+ where,
74
+ orderBy: { createdAt: 'desc' },
75
+ skip,
76
+ take: limit,
77
+ include,
78
+ }),
79
+ this._errors.model.errors.count({ where }),
80
+ ]);
81
+ return {
82
+ items,
83
+ total,
84
+ page,
85
+ limit,
86
+ hasMore: skip + items.length < total,
87
+ };
88
+ }
89
+
90
+ const unknownWhere = { ...where, message: { contains: UNKNOWN_TOKEN } };
91
+ const knownWhere = {
92
+ ...where,
93
+ NOT: { message: { contains: UNKNOWN_TOKEN } },
94
+ };
95
+
96
+ const [unknownTotal, knownTotal] = await Promise.all([
97
+ this._errors.model.errors.count({ where: unknownWhere }),
98
+ this._errors.model.errors.count({ where: knownWhere }),
99
+ ]);
100
+
101
+ let unknownItems: any[] = [];
102
+ let knownItems: any[] = [];
103
+
104
+ if (skip < unknownTotal) {
105
+ const takeUnknown = Math.min(unknownTotal - skip, limit);
106
+ unknownItems = await this._errors.model.errors.findMany({
107
+ where: unknownWhere,
108
+ orderBy: { createdAt: 'desc' },
109
+ skip,
110
+ take: takeUnknown,
111
+ include,
112
+ });
113
+ const remaining = limit - unknownItems.length;
114
+ if (remaining > 0) {
115
+ knownItems = await this._errors.model.errors.findMany({
116
+ where: knownWhere,
117
+ orderBy: { createdAt: 'desc' },
118
+ skip: 0,
119
+ take: remaining,
120
+ include,
121
+ });
122
+ }
123
+ } else {
124
+ knownItems = await this._errors.model.errors.findMany({
125
+ where: knownWhere,
126
+ orderBy: { createdAt: 'desc' },
127
+ skip: skip - unknownTotal,
128
+ take: limit,
129
+ include,
130
+ });
131
+ }
132
+
133
+ const items = [...unknownItems, ...knownItems];
134
+ const total = unknownTotal + knownTotal;
135
+ return {
136
+ items,
137
+ total,
138
+ page,
139
+ limit,
140
+ hasMore: skip + items.length < total,
141
+ };
142
+ }
143
+ }
libraries/nestjs-libraries/src/database/prisma/errors/errors.service.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ErrorsRepository } from '@gitroom/nestjs-libraries/database/prisma/errors/errors.repository';
3
+
4
+ @Injectable()
5
+ export class ErrorsService {
6
+ constructor(private _errorsRepository: ErrorsRepository) {}
7
+
8
+ listErrors(params: {
9
+ page?: number;
10
+ limit?: number;
11
+ platform?: string;
12
+ email?: string;
13
+ unknownFirst?: boolean;
14
+ }) {
15
+ return this._errorsRepository.listErrors(params);
16
+ }
17
+
18
+ listPlatforms() {
19
+ return this._errorsRepository.listPlatforms();
20
+ }
21
+ }
libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import dayjs from 'dayjs';
4
+ import { Integration } from '@prisma/client';
5
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
6
+ import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
7
+ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
8
+ import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
9
+
10
+ @Injectable()
11
+ export class IntegrationRepository {
12
+ private storage = UploadFactory.createStorage();
13
+ constructor(
14
+ private _integration: PrismaRepository<'integration'>,
15
+ private _posts: PrismaRepository<'post'>,
16
+ private _plugs: PrismaRepository<'plugs'>,
17
+ private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
18
+ private _customers: PrismaRepository<'customer'>,
19
+ private _mentions: PrismaRepository<'mentions'>
20
+ ) {}
21
+
22
+ getMentions(platform: string, q: string) {
23
+ return this._mentions.model.mentions.findMany({
24
+ where: {
25
+ platform,
26
+ OR: [
27
+ {
28
+ name: {
29
+ contains: q,
30
+ mode: 'insensitive',
31
+ },
32
+ },
33
+ {
34
+ username: {
35
+ contains: q,
36
+ mode: 'insensitive',
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ orderBy: {
42
+ name: 'asc',
43
+ },
44
+ take: 100,
45
+ select: {
46
+ name: true,
47
+ username: true,
48
+ image: true,
49
+ },
50
+ });
51
+ }
52
+
53
+ insertMentions(
54
+ platform: string,
55
+ mentions: { name: string; username: string; image: string }[]
56
+ ) {
57
+ if (mentions.length === 0) {
58
+ return [] as any[];
59
+ }
60
+ return this._mentions.model.mentions.createMany({
61
+ data: mentions.map((mention) => ({
62
+ platform,
63
+ name: mention.name,
64
+ username: mention.username,
65
+ image: mention.image,
66
+ })),
67
+ skipDuplicates: true,
68
+ });
69
+ }
70
+
71
+ async checkPreviousConnections(org: string, id: string) {
72
+ const findIt = await this._integration.model.integration.findMany({
73
+ where: {
74
+ rootInternalId: id,
75
+ },
76
+ select: {
77
+ organizationId: true,
78
+ id: true,
79
+ },
80
+ });
81
+
82
+ if (findIt.some((f) => f.organizationId === org)) {
83
+ return false;
84
+ }
85
+
86
+ return findIt.length > 0;
87
+ }
88
+
89
+ updateProviderSettings(org: string, id: string, settings: string) {
90
+ return this._integration.model.integration.update({
91
+ where: {
92
+ id,
93
+ organizationId: org,
94
+ },
95
+ data: {
96
+ additionalSettings: settings,
97
+ },
98
+ });
99
+ }
100
+
101
+ async setTimes(org: string, id: string, times: IntegrationTimeDto) {
102
+ return this._integration.model.integration.update({
103
+ select: {
104
+ id: true,
105
+ },
106
+ where: {
107
+ id,
108
+ organizationId: org,
109
+ },
110
+ data: {
111
+ postingTimes: JSON.stringify(times.time),
112
+ },
113
+ });
114
+ }
115
+
116
+ getPlug(plugId: string) {
117
+ return this._plugs.model.plugs.findFirst({
118
+ where: {
119
+ id: plugId,
120
+ },
121
+ include: {
122
+ integration: true,
123
+ },
124
+ });
125
+ }
126
+
127
+ async getPlugs(orgId: string, integrationId: string) {
128
+ return this._plugs.model.plugs.findMany({
129
+ where: {
130
+ integrationId,
131
+ organizationId: orgId,
132
+ activated: true,
133
+ },
134
+ include: {
135
+ integration: {
136
+ select: {
137
+ id: true,
138
+ providerIdentifier: true,
139
+ },
140
+ },
141
+ },
142
+ });
143
+ }
144
+
145
+ async updateIntegration(id: string, params: Partial<Integration>) {
146
+ if (
147
+ params.picture &&
148
+ (params.picture.indexOf(process.env.CLOUDFLARE_BUCKET_URL!) === -1 ||
149
+ params.picture.indexOf(process.env.FRONTEND_URL!) === -1)
150
+ ) {
151
+ params.picture = await this.storage.uploadSimple(params.picture);
152
+ }
153
+
154
+ const existing = await this._integration.model.integration.findUnique({
155
+ where: {
156
+ organizationId_internalId: {
157
+ organizationId: params.organizationId!,
158
+ internalId: params.internalId,
159
+ },
160
+ },
161
+ });
162
+
163
+ if (existing) {
164
+ await this._posts.model.post.updateMany({
165
+ where: {
166
+ integrationId: id,
167
+ },
168
+ data: {
169
+ deletedAt: new Date(),
170
+ },
171
+ });
172
+
173
+ await this._integration.model.integration.update({
174
+ where: {
175
+ id,
176
+ },
177
+ data: {
178
+ internalId: `deleted_${params.internalId}_${makeId(10)}`,
179
+ deletedAt: new Date(),
180
+ },
181
+ });
182
+ }
183
+
184
+ return this._integration.model.integration.update({
185
+ where: {
186
+ ...(existing ? { id: existing.id } : { id }),
187
+ },
188
+ data: {
189
+ ...params,
190
+ disabled: false,
191
+ deletedAt: null,
192
+ },
193
+ });
194
+ }
195
+
196
+ disconnectChannel(org: string, id: string) {
197
+ return this._integration.model.integration.update({
198
+ where: {
199
+ id,
200
+ organizationId: org,
201
+ },
202
+ data: {
203
+ refreshNeeded: true,
204
+ },
205
+ });
206
+ }
207
+
208
+ async createOrUpdateIntegration(
209
+ additionalSettings:
210
+ | {
211
+ title: string;
212
+ description: string;
213
+ type: 'checkbox' | 'text' | 'textarea';
214
+ value: any;
215
+ regex?: string;
216
+ }[]
217
+ | undefined,
218
+ oneTimeToken: boolean,
219
+ org: string,
220
+ name: string,
221
+ picture: string | undefined,
222
+ type: 'article' | 'social',
223
+ internalId: string,
224
+ provider: string,
225
+ token: string,
226
+ refreshToken = '',
227
+ expiresIn = 999999999,
228
+ username?: string,
229
+ isBetweenSteps = false,
230
+ refresh?: string,
231
+ timezone?: number,
232
+ customInstanceDetails?: string
233
+ ) {
234
+ const postTimes = timezone
235
+ ? {
236
+ postingTimes: JSON.stringify([
237
+ { time: 560 - timezone },
238
+ { time: 850 - timezone },
239
+ { time: 1140 - timezone },
240
+ ]),
241
+ }
242
+ : {};
243
+ const upsert = await this._integration.model.integration.upsert({
244
+ where: {
245
+ organizationId_internalId: {
246
+ internalId,
247
+ organizationId: org,
248
+ },
249
+ },
250
+ create: {
251
+ type: type as any,
252
+ name,
253
+ providerIdentifier: provider,
254
+ token,
255
+ profile: username,
256
+ ...(picture ? { picture } : {}),
257
+ inBetweenSteps: isBetweenSteps,
258
+ refreshToken,
259
+ ...(expiresIn
260
+ ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
261
+ : {}),
262
+ internalId,
263
+ ...postTimes,
264
+ organizationId: org,
265
+ refreshNeeded: false,
266
+ rootInternalId: internalId,
267
+ ...(customInstanceDetails ? { customInstanceDetails } : {}),
268
+ additionalSettings: additionalSettings
269
+ ? JSON.stringify(additionalSettings)
270
+ : '[]',
271
+ },
272
+ update: {
273
+ ...(additionalSettings
274
+ ? { additionalSettings: JSON.stringify(additionalSettings) }
275
+ : {}),
276
+ ...(customInstanceDetails ? { customInstanceDetails } : {}),
277
+ type: type as any,
278
+ ...(!refresh
279
+ ? {
280
+ inBetweenSteps: isBetweenSteps,
281
+ }
282
+ : {}),
283
+ ...(picture ? { picture } : {}),
284
+ profile: username,
285
+ providerIdentifier: provider,
286
+ token,
287
+ refreshToken,
288
+ ...(expiresIn
289
+ ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
290
+ : {}),
291
+ internalId,
292
+ organizationId: org,
293
+ deletedAt: null,
294
+ refreshNeeded: false,
295
+ },
296
+ });
297
+
298
+ if (oneTimeToken) {
299
+ const rootId =
300
+ (
301
+ await this._integration.model.integration.findFirst({
302
+ where: {
303
+ organizationId: org,
304
+ internalId: internalId,
305
+ },
306
+ })
307
+ )?.rootInternalId || internalId;
308
+
309
+ await this._integration.model.integration.updateMany({
310
+ where: {
311
+ id: {
312
+ not: upsert.id,
313
+ },
314
+ rootInternalId: rootId,
315
+ },
316
+ data: {
317
+ token,
318
+ refreshToken,
319
+ refreshNeeded: false,
320
+ ...(expiresIn
321
+ ? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
322
+ : {}),
323
+ },
324
+ });
325
+ }
326
+
327
+ return upsert;
328
+ }
329
+
330
+ needsToBeRefreshed() {
331
+ return this._integration.model.integration.findMany({
332
+ where: {
333
+ tokenExpiration: {
334
+ lte: dayjs().add(1, 'day').toDate(),
335
+ },
336
+ inBetweenSteps: false,
337
+ deletedAt: null,
338
+ refreshNeeded: false,
339
+ },
340
+ });
341
+ }
342
+
343
+ async setBetweenRefreshSteps(id: string) {
344
+ return this._integration.model.integration.update({
345
+ where: {
346
+ id,
347
+ },
348
+ data: {
349
+ inBetweenSteps: true,
350
+ },
351
+ });
352
+ }
353
+ refreshNeeded(org: string, id: string) {
354
+ return this._integration.model.integration.update({
355
+ where: {
356
+ id,
357
+ organizationId: org,
358
+ },
359
+ data: {
360
+ refreshNeeded: true,
361
+ },
362
+ });
363
+ }
364
+
365
+ updateNameAndUrl(id: string, name: string, url: string) {
366
+ return this._integration.model.integration.update({
367
+ where: {
368
+ id,
369
+ },
370
+ data: {
371
+ ...(name ? { name } : {}),
372
+ ...(url ? { picture: url } : {}),
373
+ },
374
+ });
375
+ }
376
+
377
+ getIntegrationById(org: string, id: string) {
378
+ return this._integration.model.integration.findFirst({
379
+ where: {
380
+ organizationId: org,
381
+ id,
382
+ },
383
+ });
384
+ }
385
+
386
+ async getIntegrationForOrder(
387
+ id: string,
388
+ order: string,
389
+ user: string,
390
+ org: string
391
+ ) {
392
+ const integration = await this._posts.model.post.findFirst({
393
+ where: {
394
+ integrationId: id,
395
+ submittedForOrder: {
396
+ id: order,
397
+ messageGroup: {
398
+ OR: [
399
+ { sellerId: user },
400
+ { buyerId: user },
401
+ { buyerOrganizationId: org },
402
+ ],
403
+ },
404
+ },
405
+ },
406
+ select: {
407
+ integration: {
408
+ select: {
409
+ id: true,
410
+ name: true,
411
+ picture: true,
412
+ inBetweenSteps: true,
413
+ providerIdentifier: true,
414
+ },
415
+ },
416
+ },
417
+ });
418
+
419
+ return integration?.integration;
420
+ }
421
+
422
+ async updateOnCustomerName(org: string, id: string, name: string) {
423
+ const customer = !name
424
+ ? undefined
425
+ : (await this._customers.model.customer.findFirst({
426
+ where: {
427
+ orgId: org,
428
+ name,
429
+ },
430
+ })) ||
431
+ (await this._customers.model.customer.create({
432
+ data: {
433
+ name,
434
+ orgId: org,
435
+ },
436
+ }));
437
+
438
+ return this._integration.model.integration.update({
439
+ where: {
440
+ id,
441
+ organizationId: org,
442
+ },
443
+ data: {
444
+ customer: !customer
445
+ ? { disconnect: true }
446
+ : {
447
+ connect: {
448
+ id: customer.id,
449
+ },
450
+ },
451
+ },
452
+ });
453
+ }
454
+
455
+ updateIntegrationGroup(org: string, id: string, group: string) {
456
+ return this._integration.model.integration.update({
457
+ where: {
458
+ id,
459
+ organizationId: org,
460
+ },
461
+ data: !group
462
+ ? {
463
+ customer: {
464
+ disconnect: true,
465
+ },
466
+ }
467
+ : {
468
+ customer: {
469
+ connect: {
470
+ id: group,
471
+ },
472
+ },
473
+ },
474
+ });
475
+ }
476
+
477
+ customers(orgId: string) {
478
+ return this._customers.model.customer.findMany({
479
+ where: {
480
+ orgId,
481
+ deletedAt: null,
482
+ },
483
+ });
484
+ }
485
+
486
+ getIntegrationsList(org: string) {
487
+ return this._integration.model.integration.findMany({
488
+ where: {
489
+ organizationId: org,
490
+ deletedAt: null,
491
+ },
492
+ include: {
493
+ customer: true,
494
+ },
495
+ });
496
+ }
497
+
498
+ async disableChannel(org: string, id: string) {
499
+ await this._integration.model.integration.update({
500
+ where: {
501
+ id,
502
+ organizationId: org,
503
+ },
504
+ data: {
505
+ disabled: true,
506
+ },
507
+ });
508
+ }
509
+
510
+ async enableChannel(org: string, id: string) {
511
+ await this._integration.model.integration.update({
512
+ where: {
513
+ id,
514
+ organizationId: org,
515
+ },
516
+ data: {
517
+ disabled: false,
518
+ },
519
+ });
520
+ }
521
+
522
+ getPostsForChannel(org: string, id: string) {
523
+ return this._posts.model.post.groupBy({
524
+ by: ['group'],
525
+ where: {
526
+ organizationId: org,
527
+ integrationId: id,
528
+ deletedAt: null,
529
+ },
530
+ });
531
+ }
532
+
533
+ deleteChannel(org: string, id: string) {
534
+ return this._integration.model.integration.update({
535
+ where: {
536
+ id,
537
+ organizationId: org,
538
+ },
539
+ data: {
540
+ deletedAt: new Date(),
541
+ },
542
+ });
543
+ }
544
+
545
+ async checkForDeletedOnceAndUpdate(org: string, page: string) {
546
+ return this._integration.model.integration.updateMany({
547
+ where: {
548
+ organizationId: org,
549
+ internalId: page,
550
+ deletedAt: {
551
+ not: null,
552
+ },
553
+ },
554
+ data: {
555
+ internalId: makeId(10),
556
+ },
557
+ });
558
+ }
559
+
560
+ async disableIntegrations(org: string, totalChannels: number) {
561
+ const getChannels = await this._integration.model.integration.findMany({
562
+ where: {
563
+ organizationId: org,
564
+ disabled: false,
565
+ deletedAt: null,
566
+ },
567
+ take: totalChannels,
568
+ select: {
569
+ id: true,
570
+ },
571
+ });
572
+
573
+ for (const channel of getChannels) {
574
+ await this._integration.model.integration.update({
575
+ where: {
576
+ id: channel.id,
577
+ },
578
+ data: {
579
+ disabled: true,
580
+ },
581
+ });
582
+ }
583
+ }
584
+
585
+ getPlugsByIntegrationId(org: string, id: string) {
586
+ return this._plugs.model.plugs.findMany({
587
+ where: {
588
+ organizationId: org,
589
+ integrationId: id,
590
+ },
591
+ });
592
+ }
593
+
594
+ createOrUpdatePlug(org: string, integrationId: string, body: PlugDto) {
595
+ return this._plugs.model.plugs.upsert({
596
+ where: {
597
+ organizationId: org,
598
+ plugFunction_integrationId: {
599
+ integrationId,
600
+ plugFunction: body.func,
601
+ },
602
+ },
603
+ create: {
604
+ integrationId,
605
+ organizationId: org,
606
+ plugFunction: body.func,
607
+ data: JSON.stringify(body.fields),
608
+ activated: true,
609
+ },
610
+ update: {
611
+ data: JSON.stringify(body.fields),
612
+ },
613
+ select: {
614
+ activated: true,
615
+ },
616
+ });
617
+ }
618
+
619
+ changePlugActivation(orgId: string, plugId: string, status: boolean) {
620
+ return this._plugs.model.plugs.update({
621
+ where: {
622
+ organizationId: orgId,
623
+ id: plugId,
624
+ },
625
+ data: {
626
+ activated: !!status,
627
+ },
628
+ });
629
+ }
630
+
631
+ async loadExisingData(
632
+ methodName: string,
633
+ integrationId: string,
634
+ id: string[]
635
+ ) {
636
+ return this._exisingPlugData.model.exisingPlugData.findMany({
637
+ where: {
638
+ integrationId,
639
+ methodName,
640
+ value: {
641
+ in: id,
642
+ },
643
+ },
644
+ });
645
+ }
646
+
647
+ async saveExisingData(
648
+ methodName: string,
649
+ integrationId: string,
650
+ value: string[]
651
+ ) {
652
+ return this._exisingPlugData.model.exisingPlugData.createMany({
653
+ data: value.map((p) => ({
654
+ integrationId,
655
+ methodName,
656
+ value: p,
657
+ })),
658
+ });
659
+ }
660
+
661
+ async getPostingTimes(orgId: string, integrationsId?: string) {
662
+ return this._integration.model.integration.findMany({
663
+ where: {
664
+ ...(integrationsId ? { id: integrationsId } : {}),
665
+ organizationId: orgId,
666
+ disabled: false,
667
+ deletedAt: null,
668
+ },
669
+ select: {
670
+ postingTimes: true,
671
+ },
672
+ });
673
+ }
674
+ }
libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts ADDED
@@ -0,0 +1,569 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ forwardRef,
3
+ HttpException,
4
+ HttpStatus,
5
+ Inject,
6
+ Injectable,
7
+ } from '@nestjs/common';
8
+ import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository';
9
+ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
10
+ import {
11
+ AnalyticsData,
12
+ SocialProvider,
13
+ } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
14
+ import { Integration, Organization } from '@prisma/client';
15
+ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
16
+ import dayjs from 'dayjs';
17
+ import { timer } from '@gitroom/helpers/utils/timer';
18
+ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
19
+ import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
20
+ import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/integration.time.dto';
21
+ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
22
+ import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
23
+ import { difference, uniq } from 'lodash';
24
+ import utc from 'dayjs/plugin/utc';
25
+ import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
26
+ import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
27
+ import { TemporalService } from 'nestjs-temporal-core';
28
+
29
+ dayjs.extend(utc);
30
+
31
+ @Injectable()
32
+ export class IntegrationService {
33
+ private storage = UploadFactory.createStorage();
34
+ constructor(
35
+ private _integrationRepository: IntegrationRepository,
36
+ private _autopostsRepository: AutopostRepository,
37
+ private _integrationManager: IntegrationManager,
38
+ private _notificationService: NotificationService,
39
+ @Inject(forwardRef(() => RefreshIntegrationService))
40
+ private _refreshIntegrationService: RefreshIntegrationService,
41
+ private _temporalService: TemporalService
42
+ ) {}
43
+
44
+ async changeActiveCron(orgId: string) {
45
+ const data = await this._autopostsRepository.getAutoposts(orgId);
46
+
47
+ for (const item of data.filter((f) => f.active)) {
48
+ try {
49
+ await this._temporalService.terminateWorkflow(`autopost-${item.id}`);
50
+ } catch (err) {}
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ getMentions(platform: string, q: string) {
57
+ return this._integrationRepository.getMentions(platform, q);
58
+ }
59
+
60
+ insertMentions(
61
+ platform: string,
62
+ mentions: { name: string; username: string; image: string }[]
63
+ ) {
64
+ return this._integrationRepository.insertMentions(platform, mentions);
65
+ }
66
+
67
+ async setTimes(
68
+ orgId: string,
69
+ integrationId: string,
70
+ times: IntegrationTimeDto
71
+ ) {
72
+ return this._integrationRepository.setTimes(orgId, integrationId, times);
73
+ }
74
+
75
+ updateProviderSettings(org: string, id: string, additionalSettings: string) {
76
+ return this._integrationRepository.updateProviderSettings(
77
+ org,
78
+ id,
79
+ additionalSettings
80
+ );
81
+ }
82
+
83
+ checkPreviousConnections(org: string, id: string) {
84
+ return this._integrationRepository.checkPreviousConnections(org, id);
85
+ }
86
+
87
+ async createOrUpdateIntegration(
88
+ additionalSettings:
89
+ | {
90
+ title: string;
91
+ description: string;
92
+ type: 'checkbox' | 'text' | 'textarea';
93
+ value: any;
94
+ regex?: string;
95
+ }[]
96
+ | undefined,
97
+ oneTimeToken: boolean,
98
+ org: string,
99
+ name: string,
100
+ picture: string | undefined,
101
+ type: 'article' | 'social',
102
+ internalId: string,
103
+ provider: string,
104
+ token: string,
105
+ refreshToken = '',
106
+ expiresIn?: number,
107
+ username?: string,
108
+ isBetweenSteps = false,
109
+ refresh?: string,
110
+ timezone?: number,
111
+ customInstanceDetails?: string
112
+ ) {
113
+ const uploadedPicture = picture
114
+ ? picture?.indexOf('imagedelivery.net') > -1
115
+ ? picture
116
+ : await this.storage.uploadSimple(picture)
117
+ : undefined;
118
+
119
+ return this._integrationRepository.createOrUpdateIntegration(
120
+ additionalSettings,
121
+ oneTimeToken,
122
+ org,
123
+ name,
124
+ uploadedPicture,
125
+ type,
126
+ internalId,
127
+ provider,
128
+ token,
129
+ refreshToken,
130
+ expiresIn,
131
+ username,
132
+ isBetweenSteps,
133
+ refresh,
134
+ timezone,
135
+ customInstanceDetails
136
+ );
137
+ }
138
+
139
+ updateIntegrationGroup(org: string, id: string, group: string) {
140
+ return this._integrationRepository.updateIntegrationGroup(org, id, group);
141
+ }
142
+
143
+ updateOnCustomerName(org: string, id: string, name: string) {
144
+ return this._integrationRepository.updateOnCustomerName(org, id, name);
145
+ }
146
+
147
+ getIntegrationsList(org: string) {
148
+ return this._integrationRepository.getIntegrationsList(org);
149
+ }
150
+
151
+ getIntegrationForOrder(id: string, order: string, user: string, org: string) {
152
+ return this._integrationRepository.getIntegrationForOrder(
153
+ id,
154
+ order,
155
+ user,
156
+ org
157
+ );
158
+ }
159
+
160
+ updateNameAndUrl(id: string, name: string, url: string) {
161
+ return this._integrationRepository.updateNameAndUrl(id, name, url);
162
+ }
163
+
164
+ getIntegrationById(org: string, id: string) {
165
+ return this._integrationRepository.getIntegrationById(org, id);
166
+ }
167
+
168
+ async refreshToken(provider: SocialProvider, refresh: string) {
169
+ try {
170
+ const { refreshToken, accessToken, expiresIn } =
171
+ await provider.refreshToken(refresh);
172
+
173
+ if (!refreshToken || !accessToken || !expiresIn) {
174
+ return false;
175
+ }
176
+
177
+ return { refreshToken, accessToken, expiresIn };
178
+ } catch (e) {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ async disconnectChannel(orgId: string, integration: Integration) {
184
+ await this._integrationRepository.disconnectChannel(orgId, integration.id);
185
+ await this.informAboutRefreshError(orgId, integration);
186
+ }
187
+
188
+ async informAboutRefreshError(
189
+ orgId: string,
190
+ integration: Integration,
191
+ err = ''
192
+ ) {
193
+ await this._notificationService.inAppNotification(
194
+ orgId,
195
+ `Could not refresh your ${integration.providerIdentifier} channel ${err}`,
196
+ `Could not refresh your ${integration.providerIdentifier} channel ${err}. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`,
197
+ true,
198
+ false,
199
+ 'info'
200
+ );
201
+ }
202
+
203
+ async refreshNeeded(org: string, id: string) {
204
+ return this._integrationRepository.refreshNeeded(org, id);
205
+ }
206
+
207
+ async setBetweenRefreshSteps(id: string) {
208
+ return this._integrationRepository.setBetweenRefreshSteps(id);
209
+ }
210
+
211
+ async refreshTokens() {
212
+ const integrations = await this._integrationRepository.needsToBeRefreshed();
213
+ for (const integration of integrations) {
214
+ const provider = this._integrationManager.getSocialIntegration(
215
+ integration.providerIdentifier
216
+ );
217
+
218
+ const data = await this.refreshToken(provider, integration.refreshToken!);
219
+
220
+ if (!data) {
221
+ await this.informAboutRefreshError(
222
+ integration.organizationId,
223
+ integration
224
+ );
225
+ await this._integrationRepository.refreshNeeded(
226
+ integration.organizationId,
227
+ integration.id
228
+ );
229
+ return;
230
+ }
231
+
232
+ const { refreshToken, accessToken, expiresIn } = data;
233
+
234
+ await this.createOrUpdateIntegration(
235
+ undefined,
236
+ !!provider.oneTimeToken,
237
+ integration.organizationId,
238
+ integration.name,
239
+ undefined,
240
+ 'social',
241
+ integration.internalId,
242
+ integration.providerIdentifier,
243
+ accessToken,
244
+ refreshToken,
245
+ expiresIn
246
+ );
247
+ }
248
+ }
249
+
250
+ async disableChannel(org: string, id: string) {
251
+ return this._integrationRepository.disableChannel(org, id);
252
+ }
253
+
254
+ async enableChannel(org: string, totalChannels: number, id: string) {
255
+ const integrations = (
256
+ await this._integrationRepository.getIntegrationsList(org)
257
+ ).filter((f) => !f.disabled);
258
+ if (
259
+ !!process.env.STRIPE_PUBLISHABLE_KEY &&
260
+ integrations.length >= totalChannels
261
+ ) {
262
+ throw new Error('You have reached the maximum number of channels');
263
+ }
264
+
265
+ return this._integrationRepository.enableChannel(org, id);
266
+ }
267
+
268
+ async getPostsForChannel(org: string, id: string) {
269
+ return this._integrationRepository.getPostsForChannel(org, id);
270
+ }
271
+
272
+ async deleteChannel(org: string, id: string) {
273
+ return this._integrationRepository.deleteChannel(org, id);
274
+ }
275
+
276
+ async disableIntegrations(org: string, totalChannels: number) {
277
+ return this._integrationRepository.disableIntegrations(org, totalChannels);
278
+ }
279
+
280
+ async checkForDeletedOnceAndUpdate(org: string, page: string) {
281
+ return this._integrationRepository.checkForDeletedOnceAndUpdate(org, page);
282
+ }
283
+
284
+ async saveProviderPage(org: string, id: string, data: any) {
285
+ const getIntegration = await this._integrationRepository.getIntegrationById(
286
+ org,
287
+ id
288
+ );
289
+ if (!getIntegration) {
290
+ throw new HttpException('Integration not found', HttpStatus.NOT_FOUND);
291
+ }
292
+ if (!getIntegration.inBetweenSteps) {
293
+ throw new HttpException('Invalid request', HttpStatus.BAD_REQUEST);
294
+ }
295
+
296
+ const provider = this._integrationManager.getSocialIntegration(
297
+ getIntegration.providerIdentifier
298
+ );
299
+
300
+ if (!provider.fetchPageInformation) {
301
+ throw new HttpException(
302
+ 'Provider does not support page selection',
303
+ HttpStatus.BAD_REQUEST
304
+ );
305
+ }
306
+
307
+ const getIntegrationInformation = await provider.fetchPageInformation(
308
+ getIntegration.token,
309
+ data
310
+ );
311
+
312
+ await this.checkForDeletedOnceAndUpdate(
313
+ org,
314
+ String(getIntegrationInformation.id)
315
+ );
316
+ await this._integrationRepository.updateIntegration(id, {
317
+ picture: getIntegrationInformation.picture,
318
+ internalId: String(getIntegrationInformation.id),
319
+ organizationId: org,
320
+ name: getIntegrationInformation.name,
321
+ inBetweenSteps: false,
322
+ token: getIntegrationInformation.access_token,
323
+ profile: getIntegrationInformation.username,
324
+ });
325
+
326
+ return { success: true };
327
+ }
328
+
329
+ async checkAnalytics(
330
+ org: Organization,
331
+ integration: string,
332
+ date: string,
333
+ forceRefresh = false
334
+ ): Promise<AnalyticsData[]> {
335
+ const getIntegration = await this.getIntegrationById(org.id, integration);
336
+
337
+ if (!getIntegration) {
338
+ throw new Error('Invalid integration');
339
+ }
340
+
341
+ if (getIntegration.type !== 'social') {
342
+ return [];
343
+ }
344
+
345
+ const integrationProvider = this._integrationManager.getSocialIntegration(
346
+ getIntegration.providerIdentifier
347
+ );
348
+
349
+ if (
350
+ dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
351
+ forceRefresh
352
+ ) {
353
+ const data = await this._refreshIntegrationService.refresh(
354
+ getIntegration
355
+ );
356
+ if (!data) {
357
+ return [];
358
+ }
359
+
360
+ const { accessToken } = data;
361
+
362
+ if (accessToken) {
363
+ getIntegration.token = accessToken;
364
+
365
+ if (integrationProvider.refreshWait) {
366
+ await timer(10000);
367
+ }
368
+ } else {
369
+ await this.disconnectChannel(org.id, getIntegration);
370
+ return [];
371
+ }
372
+ }
373
+
374
+ const getIntegrationData = await ioRedis.get(
375
+ `integration:${org.id}:${integration}:${date}`
376
+ );
377
+ if (getIntegrationData) {
378
+ return JSON.parse(getIntegrationData);
379
+ }
380
+
381
+ if (integrationProvider.analytics) {
382
+ try {
383
+ const loadAnalytics = await integrationProvider.analytics(
384
+ getIntegration.internalId,
385
+ getIntegration.token,
386
+ +date
387
+ );
388
+ await ioRedis.set(
389
+ `integration:${org.id}:${integration}:${date}`,
390
+ JSON.stringify(loadAnalytics),
391
+ 'EX',
392
+ !process.env.NODE_ENV || process.env.NODE_ENV === 'development'
393
+ ? 1
394
+ : 3600
395
+ );
396
+ return loadAnalytics;
397
+ } catch (e) {
398
+ if (e instanceof RefreshToken) {
399
+ return this.checkAnalytics(org, integration, date, true);
400
+ }
401
+ }
402
+ }
403
+
404
+ return [];
405
+ }
406
+
407
+ customers(orgId: string) {
408
+ return this._integrationRepository.customers(orgId);
409
+ }
410
+
411
+ getPlugsByIntegrationId(org: string, integrationId: string) {
412
+ return this._integrationRepository.getPlugsByIntegrationId(
413
+ org,
414
+ integrationId
415
+ );
416
+ }
417
+
418
+ async processInternalPlug(
419
+ data: {
420
+ post: string;
421
+ originalIntegration: string;
422
+ integration: string;
423
+ plugName: string;
424
+ orgId: string;
425
+ delay: number;
426
+ information: any;
427
+ },
428
+ forceRefresh = false
429
+ ): Promise<any> {
430
+ const originalIntegration =
431
+ await this._integrationRepository.getIntegrationById(
432
+ data.orgId,
433
+ data.originalIntegration
434
+ );
435
+
436
+ const getIntegration = await this._integrationRepository.getIntegrationById(
437
+ data.orgId,
438
+ data.integration
439
+ );
440
+
441
+ if (!getIntegration || !originalIntegration) {
442
+ return;
443
+ }
444
+
445
+ const getAllInternalPlugs = this._integrationManager
446
+ .getInternalPlugs(getIntegration.providerIdentifier)
447
+ .internalPlugs.find((p: any) => p.identifier === data.plugName);
448
+
449
+ if (!getAllInternalPlugs) {
450
+ return;
451
+ }
452
+
453
+ const getSocialIntegration = this._integrationManager.getSocialIntegration(
454
+ getIntegration.providerIdentifier
455
+ );
456
+
457
+ // @ts-ignore
458
+ await getSocialIntegration?.[getAllInternalPlugs.methodName]?.(
459
+ getIntegration,
460
+ originalIntegration,
461
+ data.post,
462
+ data.information
463
+ );
464
+
465
+ return;
466
+ }
467
+
468
+ async processPlugs(data: {
469
+ plugId: string;
470
+ postId: string;
471
+ delay: number;
472
+ totalRuns: number;
473
+ currentRun: number;
474
+ }) {
475
+ const getPlugById = await this._integrationRepository.getPlug(data.plugId);
476
+ if (!getPlugById) {
477
+ return true;
478
+ }
479
+
480
+ const integration = this._integrationManager.getSocialIntegration(
481
+ getPlugById.integration.providerIdentifier
482
+ );
483
+
484
+ // @ts-ignore
485
+ const process = await integration[getPlugById.plugFunction](
486
+ getPlugById.integration,
487
+ data.postId,
488
+ JSON.parse(getPlugById.data).reduce((all: any, current: any) => {
489
+ all[current.name] = current.value;
490
+ return all;
491
+ }, {})
492
+ );
493
+
494
+ if (process) {
495
+ return true;
496
+ }
497
+
498
+ if (data.totalRuns === data.currentRun) {
499
+ return true;
500
+ }
501
+
502
+ return false;
503
+ }
504
+
505
+ async createOrUpdatePlug(
506
+ orgId: string,
507
+ integrationId: string,
508
+ body: PlugDto
509
+ ) {
510
+ const { activated } = await this._integrationRepository.createOrUpdatePlug(
511
+ orgId,
512
+ integrationId,
513
+ body
514
+ );
515
+
516
+ return {
517
+ activated,
518
+ };
519
+ }
520
+
521
+ async changePlugActivation(orgId: string, plugId: string, status: boolean) {
522
+ const { id, integrationId, plugFunction } =
523
+ await this._integrationRepository.changePlugActivation(
524
+ orgId,
525
+ plugId,
526
+ status
527
+ );
528
+
529
+ return { id };
530
+ }
531
+
532
+ async getPlugs(orgId: string, integrationId: string) {
533
+ return this._integrationRepository.getPlugs(orgId, integrationId);
534
+ }
535
+
536
+ async loadExisingData(
537
+ methodName: string,
538
+ integrationId: string,
539
+ id: string[]
540
+ ) {
541
+ const exisingData = await this._integrationRepository.loadExisingData(
542
+ methodName,
543
+ integrationId,
544
+ id
545
+ );
546
+ const loadOnlyIds = exisingData.map((p) => p.value);
547
+ return difference(id, loadOnlyIds);
548
+ }
549
+
550
+ async findFreeDateTime(
551
+ orgId: string,
552
+ integrationsId?: string
553
+ ): Promise<number[]> {
554
+ const findTimes = await this._integrationRepository.getPostingTimes(
555
+ orgId,
556
+ integrationsId
557
+ );
558
+ return uniq(
559
+ findTimes.reduce((all: any, current: any) => {
560
+ return [
561
+ ...all,
562
+ ...JSON.parse(current.postingTimes).map(
563
+ (p: { time: number }) => p.time
564
+ ),
565
+ ];
566
+ }, [] as number[])
567
+ );
568
+ }
569
+ }
libraries/nestjs-libraries/src/database/prisma/media/media.repository.ts ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto';
4
+
5
+ @Injectable()
6
+ export class MediaRepository {
7
+ constructor(private _media: PrismaRepository<'media'>) {}
8
+
9
+ saveFile(org: string, fileName: string, filePath: string, originalName?: string) {
10
+ return this._media.model.media.create({
11
+ data: {
12
+ organization: {
13
+ connect: {
14
+ id: org,
15
+ },
16
+ },
17
+ name: fileName,
18
+ path: filePath,
19
+ originalName: originalName || null,
20
+ },
21
+ select: {
22
+ id: true,
23
+ name: true,
24
+ originalName: true,
25
+ path: true,
26
+ thumbnail: true,
27
+ alt: true,
28
+ },
29
+ });
30
+ }
31
+
32
+ getMediaById(id: string) {
33
+ return this._media.model.media.findUnique({
34
+ where: {
35
+ id,
36
+ },
37
+ });
38
+ }
39
+
40
+ deleteMedia(org: string, id: string) {
41
+ return this._media.model.media.update({
42
+ where: {
43
+ id,
44
+ organizationId: org,
45
+ },
46
+ data: {
47
+ deletedAt: new Date(),
48
+ },
49
+ });
50
+ }
51
+
52
+ saveMediaInformation(org: string, data: SaveMediaInformationDto) {
53
+ return this._media.model.media.update({
54
+ where: {
55
+ id: data.id,
56
+ organizationId: org,
57
+ },
58
+ data: {
59
+ alt: data.alt,
60
+ thumbnail: data.thumbnail,
61
+ thumbnailTimestamp: data.thumbnailTimestamp,
62
+ },
63
+ select: {
64
+ id: true,
65
+ name: true,
66
+ originalName: true,
67
+ alt: true,
68
+ thumbnail: true,
69
+ path: true,
70
+ thumbnailTimestamp: true,
71
+ },
72
+ });
73
+ }
74
+
75
+ async getMedia(org: string, page: number, search?: string) {
76
+ const pageNum = (page || 1) - 1;
77
+ const trimmedSearch = search?.trim();
78
+ const searchFilter = trimmedSearch
79
+ ? {
80
+ originalName: {
81
+ contains: trimmedSearch,
82
+ mode: 'insensitive' as const,
83
+ },
84
+ }
85
+ : {};
86
+ const query = {
87
+ where: {
88
+ organization: {
89
+ id: org,
90
+ },
91
+ deletedAt: null,
92
+ ...searchFilter,
93
+ },
94
+ };
95
+ const pages = Math.ceil((await this._media.model.media.count(query)) / 18);
96
+ const results = await this._media.model.media.findMany({
97
+ where: {
98
+ organizationId: org,
99
+ deletedAt: null,
100
+ ...searchFilter,
101
+ },
102
+ orderBy: {
103
+ createdAt: 'desc',
104
+ },
105
+ select: {
106
+ id: true,
107
+ name: true,
108
+ originalName: true,
109
+ path: true,
110
+ thumbnail: true,
111
+ alt: true,
112
+ thumbnailTimestamp: true,
113
+ },
114
+ skip: pageNum * 18,
115
+ take: 18,
116
+ });
117
+
118
+ return {
119
+ pages,
120
+ results,
121
+ };
122
+ }
123
+ }
libraries/nestjs-libraries/src/database/prisma/media/media.service.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpException, Injectable } from '@nestjs/common';
2
+ import { MediaRepository } from '@gitroom/nestjs-libraries/database/prisma/media/media.repository';
3
+ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
4
+ import { generationError } from '@gitroom/nestjs-libraries/openai/generation.error';
5
+ import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
6
+ import { Organization } from '@prisma/client';
7
+ import { SaveMediaInformationDto } from '@gitroom/nestjs-libraries/dtos/media/save.media.information.dto';
8
+ import { VideoManager } from '@gitroom/nestjs-libraries/videos/video.manager';
9
+ import { VideoDto } from '@gitroom/nestjs-libraries/dtos/videos/video.dto';
10
+ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
11
+ import {
12
+ AuthorizationActions,
13
+ Sections,
14
+ SubscriptionException,
15
+ } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
16
+
17
+ @Injectable()
18
+ export class MediaService {
19
+ private storage = UploadFactory.createStorage();
20
+
21
+ constructor(
22
+ private _mediaRepository: MediaRepository,
23
+ private _openAi: OpenaiService,
24
+ private _subscriptionService: SubscriptionService,
25
+ private _videoManager: VideoManager
26
+ ) {}
27
+
28
+ async deleteMedia(org: string, id: string) {
29
+ return this._mediaRepository.deleteMedia(org, id);
30
+ }
31
+
32
+ getMediaById(id: string) {
33
+ return this._mediaRepository.getMediaById(id);
34
+ }
35
+
36
+ async generateImage(
37
+ prompt: string,
38
+ org: Organization,
39
+ generatePromptFirst?: boolean
40
+ ) {
41
+ try {
42
+ const generating = await this._subscriptionService.useCredit(
43
+ org,
44
+ 'ai_images',
45
+ async () => {
46
+ if (generatePromptFirst) {
47
+ prompt = await this._openAi.generatePromptForPicture(prompt);
48
+ console.log('Prompt:', prompt);
49
+ }
50
+ return this._openAi.generateImage(prompt);
51
+ }
52
+ );
53
+
54
+ return generating;
55
+ } catch (err) {
56
+ throw generationError(err);
57
+ }
58
+ }
59
+
60
+ saveFile(org: string, fileName: string, filePath: string, originalName?: string) {
61
+ return this._mediaRepository.saveFile(org, fileName, filePath, originalName);
62
+ }
63
+
64
+ getMedia(org: string, page: number, search?: string) {
65
+ return this._mediaRepository.getMedia(org, page, search);
66
+ }
67
+
68
+ saveMediaInformation(org: string, data: SaveMediaInformationDto) {
69
+ return this._mediaRepository.saveMediaInformation(org, data);
70
+ }
71
+
72
+ getVideoOptions() {
73
+ return this._videoManager.getAllVideos();
74
+ }
75
+
76
+ async generateVideoAllowed(org: Organization, type: string) {
77
+ const video = this._videoManager.getVideoByName(type);
78
+ if (!video) {
79
+ throw new Error(`Video type ${type} not found`);
80
+ }
81
+
82
+ if (!video.trial && org.isTrailing) {
83
+ throw new HttpException('This video is not available in trial mode', 406);
84
+ }
85
+
86
+ return true;
87
+ }
88
+
89
+ async generateVideo(org: Organization, body: VideoDto) {
90
+ try {
91
+ const totalCredits = await this._subscriptionService.checkCredits(
92
+ org,
93
+ 'ai_videos'
94
+ );
95
+
96
+ if (totalCredits.credits <= 0) {
97
+ throw new SubscriptionException({
98
+ action: AuthorizationActions.Create,
99
+ section: Sections.VIDEOS_PER_MONTH,
100
+ });
101
+ }
102
+
103
+ const video = this._videoManager.getVideoByName(body.type);
104
+ if (!video) {
105
+ throw new Error(`Video type ${body.type} not found`);
106
+ }
107
+
108
+ if (!video.trial && org.isTrailing) {
109
+ throw new HttpException(
110
+ 'This video is not available in trial mode',
111
+ 406
112
+ );
113
+ }
114
+
115
+ console.log(body.customParams);
116
+ await video.instance.processAndValidate(body.customParams);
117
+ console.log('no err');
118
+
119
+ return await this._subscriptionService.useCredit(
120
+ org,
121
+ 'ai_videos',
122
+ async () => {
123
+ const loadedData = await video.instance.process(
124
+ body.output,
125
+ body.customParams
126
+ );
127
+
128
+ const file = await this.storage.uploadSimple(loadedData);
129
+ return this.saveFile(org.id, file.split('/').pop(), file);
130
+ }
131
+ );
132
+ } catch (err) {
133
+ throw generationError(err);
134
+ }
135
+ }
136
+
137
+ async videoFunction(identifier: string, functionName: string, body: any) {
138
+ const video = this._videoManager.getVideoByName(identifier);
139
+ if (!video) {
140
+ throw new Error(`Video with identifier ${identifier} not found`);
141
+ }
142
+
143
+ // @ts-ignore
144
+ const functionToCall = video.instance[functionName];
145
+ if (
146
+ typeof functionToCall !== 'function' ||
147
+ this._videoManager.checkAvailableVideoFunction(functionToCall)
148
+ ) {
149
+ throw new HttpException(
150
+ `Function ${functionName} not found on video instance`,
151
+ 400
152
+ );
153
+ }
154
+
155
+ return functionToCall(body);
156
+ }
157
+ }
libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { NotificationsRepository } from '@gitroom/nestjs-libraries/database/prisma/notifications/notifications.repository';
3
+ import { EmailService } from '@gitroom/nestjs-libraries/services/email.service';
4
+ import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
5
+ import { TemporalService } from 'nestjs-temporal-core';
6
+ import { TypedSearchAttributes } from '@temporalio/common';
7
+ import { organizationId } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
8
+
9
+ export type NotificationType = 'success' | 'fail' | 'info';
10
+
11
+ @Injectable()
12
+ export class NotificationService {
13
+ constructor(
14
+ private _notificationRepository: NotificationsRepository,
15
+ private _emailService: EmailService,
16
+ private _organizationRepository: OrganizationRepository,
17
+ private _temporalService: TemporalService
18
+ ) {}
19
+
20
+ getMainPageCount(organizationId: string, userId: string) {
21
+ return this._notificationRepository.getMainPageCount(
22
+ organizationId,
23
+ userId
24
+ );
25
+ }
26
+
27
+ getNotificationsPaginated(organizationId: string, page: number) {
28
+ return this._notificationRepository.getNotificationsPaginated(
29
+ organizationId,
30
+ page
31
+ );
32
+ }
33
+
34
+ getNotifications(organizationId: string, userId: string) {
35
+ return this._notificationRepository.getNotifications(
36
+ organizationId,
37
+ userId
38
+ );
39
+ }
40
+
41
+ async inAppNotification(
42
+ orgId: string,
43
+ subject: string,
44
+ message: string,
45
+ sendEmail = false,
46
+ digest = false,
47
+ type: NotificationType = 'success'
48
+ ) {
49
+ await this._notificationRepository.createNotification(orgId, message);
50
+ if (!sendEmail) {
51
+ return;
52
+ }
53
+
54
+ if (digest) {
55
+ try {
56
+ await this._temporalService.client
57
+ .getRawClient()
58
+ ?.workflow.signalWithStart('digestEmailWorkflow', {
59
+ workflowId: 'digest_email_workflow_' + orgId,
60
+ signal: 'email',
61
+ signalArgs: [
62
+ [
63
+ {
64
+ title: subject,
65
+ message,
66
+ type,
67
+ },
68
+ ],
69
+ ],
70
+ taskQueue: 'main',
71
+ workflowIdConflictPolicy: 'USE_EXISTING',
72
+ args: [{ organizationId: orgId }],
73
+ typedSearchAttributes: new TypedSearchAttributes([
74
+ {
75
+ key: organizationId,
76
+ value: orgId,
77
+ },
78
+ ]),
79
+ });
80
+ } catch (err) {}
81
+
82
+ return;
83
+ }
84
+
85
+ await this.sendEmailsToOrg(orgId, subject, message, type);
86
+ }
87
+
88
+ async sendEmailsToOrg(
89
+ orgId: string,
90
+ subject: string,
91
+ message: string,
92
+ type?: NotificationType
93
+ ) {
94
+ const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId);
95
+ for (const user of userOrg?.users || []) {
96
+ // 'info' type is always sent regardless of preferences
97
+ if (type !== 'info') {
98
+ // Filter users based on their email preferences
99
+ if (type === 'success' && !user.user.sendSuccessEmails) {
100
+ continue;
101
+ }
102
+ if (type === 'fail' && !user.user.sendFailureEmails) {
103
+ continue;
104
+ }
105
+ }
106
+ await this.sendEmail(user.user.email, subject, message);
107
+ }
108
+ }
109
+
110
+ async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
111
+ await this._emailService.sendEmail(to, subject, html, 'top', replyTo);
112
+ }
113
+
114
+ hasEmailProvider() {
115
+ return this._emailService.hasProvider();
116
+ }
117
+ }
libraries/nestjs-libraries/src/database/prisma/notifications/notifications.repository.ts ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+
4
+ @Injectable()
5
+ export class NotificationsRepository {
6
+ constructor(
7
+ private _notifications: PrismaRepository<'notifications'>,
8
+ private _user: PrismaRepository<'user'>
9
+ ) {}
10
+
11
+ getLastReadNotification(userId: string) {
12
+ return this._user.model.user.findFirst({
13
+ where: {
14
+ id: userId,
15
+ },
16
+ select: {
17
+ lastReadNotifications: true,
18
+ },
19
+ });
20
+ }
21
+
22
+ async getMainPageCount(organizationId: string, userId: string) {
23
+ const { lastReadNotifications } = (await this.getLastReadNotification(
24
+ userId
25
+ ))!;
26
+
27
+ return {
28
+ total: await this._notifications.model.notifications.count({
29
+ where: {
30
+ organizationId,
31
+ createdAt: {
32
+ gt: lastReadNotifications!,
33
+ },
34
+ },
35
+ }),
36
+ };
37
+ }
38
+
39
+ async createNotification(organizationId: string, content: string) {
40
+ await this._notifications.model.notifications.create({
41
+ data: {
42
+ organizationId,
43
+ content,
44
+ },
45
+ });
46
+ }
47
+
48
+ async getNotificationsSince(organizationId: string, since: string) {
49
+ return this._notifications.model.notifications.findMany({
50
+ where: {
51
+ organizationId,
52
+ createdAt: {
53
+ gte: new Date(since),
54
+ },
55
+ },
56
+ });
57
+ }
58
+
59
+ async getNotificationsPaginated(organizationId: string, page: number) {
60
+ const limit = 100;
61
+ const skip = page * limit;
62
+
63
+ const where = {
64
+ organizationId,
65
+ deletedAt: null as Date | null,
66
+ };
67
+
68
+ const [notifications, total] = await Promise.all([
69
+ this._notifications.model.notifications.findMany({
70
+ where,
71
+ orderBy: {
72
+ createdAt: 'desc',
73
+ },
74
+ skip,
75
+ take: limit,
76
+ select: {
77
+ id: true,
78
+ content: true,
79
+ link: true,
80
+ createdAt: true,
81
+ },
82
+ }),
83
+ this._notifications.model.notifications.count({ where }),
84
+ ]);
85
+
86
+ return {
87
+ notifications,
88
+ total,
89
+ page,
90
+ limit,
91
+ hasMore: skip + notifications.length < total,
92
+ };
93
+ }
94
+
95
+ async getNotifications(organizationId: string, userId: string) {
96
+ const { lastReadNotifications } = (await this.getLastReadNotification(
97
+ userId
98
+ ))!;
99
+
100
+ await this._user.model.user.update({
101
+ where: {
102
+ id: userId,
103
+ },
104
+ data: {
105
+ lastReadNotifications: new Date(),
106
+ },
107
+ });
108
+
109
+ return {
110
+ lastReadNotifications,
111
+ notifications: await this._notifications.model.notifications.findMany({
112
+ orderBy: {
113
+ createdAt: 'desc',
114
+ },
115
+ take: 10,
116
+ where: {
117
+ organizationId,
118
+ },
119
+ select: {
120
+ createdAt: true,
121
+ content: true,
122
+ },
123
+ }),
124
+ };
125
+ }
126
+ }
libraries/nestjs-libraries/src/database/prisma/oauth/oauth.repository.ts ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
3
+
4
+ @Injectable()
5
+ export class OAuthRepository {
6
+ constructor(
7
+ private _oauthApp: PrismaRepository<'oAuthApp'>,
8
+ private _oauthAuth: PrismaRepository<'oAuthAuthorization'>
9
+ ) {}
10
+
11
+ getAppByOrgId(orgId: string) {
12
+ return this._oauthApp.model.oAuthApp.findFirst({
13
+ where: {
14
+ organizationId: orgId,
15
+ deletedAt: null,
16
+ },
17
+ include: {
18
+ picture: true,
19
+ },
20
+ });
21
+ }
22
+
23
+ getAppByClientId(clientId: string) {
24
+ return this._oauthApp.model.oAuthApp.findFirst({
25
+ where: {
26
+ clientId,
27
+ deletedAt: null,
28
+ },
29
+ include: {
30
+ picture: true,
31
+ },
32
+ });
33
+ }
34
+
35
+ createApp(
36
+ orgId: string,
37
+ data: {
38
+ name: string;
39
+ description?: string;
40
+ pictureId?: string;
41
+ redirectUrl: string;
42
+ clientId: string;
43
+ clientSecret: string;
44
+ }
45
+ ) {
46
+ return this._oauthApp.model.oAuthApp.create({
47
+ data: {
48
+ organizationId: orgId,
49
+ name: data.name,
50
+ description: data.description,
51
+ pictureId: data.pictureId,
52
+ redirectUrl: data.redirectUrl,
53
+ clientId: data.clientId,
54
+ clientSecret: data.clientSecret,
55
+ },
56
+ include: {
57
+ picture: true,
58
+ },
59
+ });
60
+ }
61
+
62
+ async updateApp(
63
+ orgId: string,
64
+ data: {
65
+ name?: string;
66
+ description?: string;
67
+ pictureId?: string;
68
+ redirectUrl?: string;
69
+ }
70
+ ) {
71
+ const app = await this._oauthApp.model.oAuthApp.findFirst({
72
+ where: {
73
+ organizationId: orgId,
74
+ deletedAt: null,
75
+ },
76
+ });
77
+ if (!app) {
78
+ return null;
79
+ }
80
+ return this._oauthApp.model.oAuthApp.update({
81
+ where: { id: app.id },
82
+ data,
83
+ include: {
84
+ picture: true,
85
+ },
86
+ });
87
+ }
88
+
89
+ async deleteApp(orgId: string) {
90
+ const app = await this._oauthApp.model.oAuthApp.findFirst({
91
+ where: {
92
+ organizationId: orgId,
93
+ deletedAt: null,
94
+ },
95
+ });
96
+ if (!app) {
97
+ return null;
98
+ }
99
+ return this._oauthApp.model.oAuthApp.update({
100
+ where: { id: app.id },
101
+ data: {
102
+ deletedAt: new Date(),
103
+ },
104
+ });
105
+ }
106
+
107
+ async updateClientSecret(orgId: string, newSecret: string) {
108
+ const app = await this._oauthApp.model.oAuthApp.findFirst({
109
+ where: {
110
+ organizationId: orgId,
111
+ deletedAt: null,
112
+ },
113
+ });
114
+ if (!app) {
115
+ return null;
116
+ }
117
+ return this._oauthApp.model.oAuthApp.update({
118
+ where: { id: app.id },
119
+ data: {
120
+ clientSecret: newSecret,
121
+ },
122
+ });
123
+ }
124
+
125
+ createAuthorization(data: {
126
+ oauthAppId: string;
127
+ userId: string;
128
+ organizationId: string;
129
+ authorizationCode: string;
130
+ codeExpiresAt: Date;
131
+ }) {
132
+ return this._oauthAuth.model.oAuthAuthorization.upsert({
133
+ where: {
134
+ oauthAppId_userId_organizationId: {
135
+ oauthAppId: data.oauthAppId,
136
+ userId: data.userId,
137
+ organizationId: data.organizationId,
138
+ },
139
+ },
140
+ create: {
141
+ oauthAppId: data.oauthAppId,
142
+ userId: data.userId,
143
+ organizationId: data.organizationId,
144
+ authorizationCode: data.authorizationCode,
145
+ codeExpiresAt: data.codeExpiresAt,
146
+ },
147
+ update: {
148
+ authorizationCode: data.authorizationCode,
149
+ codeExpiresAt: data.codeExpiresAt,
150
+ accessToken: null,
151
+ revokedAt: null,
152
+ },
153
+ });
154
+ }
155
+
156
+ findByCode(encryptedCode: string) {
157
+ return this._oauthAuth.model.oAuthAuthorization.findFirst({
158
+ where: {
159
+ authorizationCode: encryptedCode,
160
+ revokedAt: null,
161
+ },
162
+ });
163
+ }
164
+
165
+ exchangeCodeForToken(id: string, encryptedToken: string) {
166
+ return this._oauthAuth.model.oAuthAuthorization.update({
167
+ where: { id },
168
+ select: {
169
+ organizationId: true,
170
+ organization: {
171
+ select: {
172
+ paymentId: true,
173
+ }
174
+ }
175
+ },
176
+ data: {
177
+ accessToken: encryptedToken,
178
+ authorizationCode: null,
179
+ codeExpiresAt: null,
180
+ },
181
+ });
182
+ }
183
+
184
+ findByAccessToken(encryptedToken: string) {
185
+ return this._oauthAuth.model.oAuthAuthorization.findFirst({
186
+ where: {
187
+ accessToken: encryptedToken,
188
+ revokedAt: null,
189
+ },
190
+ include: {
191
+ organization: {
192
+ include: {
193
+ subscription: {
194
+ select: {
195
+ subscriptionTier: true,
196
+ totalChannels: true,
197
+ isLifetime: true,
198
+ },
199
+ },
200
+ },
201
+ },
202
+ user: {
203
+ select: { id: true },
204
+ },
205
+ },
206
+ });
207
+ }
208
+
209
+ getApprovedApps(userId: string) {
210
+ return this._oauthAuth.model.oAuthAuthorization.findMany({
211
+ where: {
212
+ userId,
213
+ revokedAt: null,
214
+ accessToken: { not: null },
215
+ },
216
+ include: {
217
+ oauthApp: {
218
+ include: {
219
+ picture: true,
220
+ },
221
+ },
222
+ },
223
+ orderBy: {
224
+ createdAt: 'desc',
225
+ },
226
+ });
227
+ }
228
+
229
+ revokeAuthorization(userId: string, authId: string) {
230
+ return this._oauthAuth.model.oAuthAuthorization.update({
231
+ where: {
232
+ id: authId,
233
+ userId,
234
+ },
235
+ data: {
236
+ revokedAt: new Date(),
237
+ },
238
+ });
239
+ }
240
+
241
+ revokeAllForApp(oauthAppId: string) {
242
+ return this._oauthAuth.model.oAuthAuthorization.updateMany({
243
+ where: {
244
+ oauthAppId,
245
+ revokedAt: null,
246
+ },
247
+ data: {
248
+ revokedAt: new Date(),
249
+ },
250
+ });
251
+ }
252
+ }
libraries/nestjs-libraries/src/database/prisma/oauth/oauth.service.ts ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2
+ import { OAuthRepository } from '@gitroom/nestjs-libraries/database/prisma/oauth/oauth.repository';
3
+ import { CreateOAuthAppDto } from '@gitroom/nestjs-libraries/dtos/oauth/create-oauth-app.dto';
4
+ import { UpdateOAuthAppDto } from '@gitroom/nestjs-libraries/dtos/oauth/update-oauth-app.dto';
5
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
6
+ import { AuthService } from '@gitroom/helpers/auth/auth.service';
7
+
8
+ @Injectable()
9
+ export class OAuthService {
10
+ constructor(private _oauthRepository: OAuthRepository) {}
11
+
12
+ async getApp(orgId: string) {
13
+ const app = await this._oauthRepository.getAppByOrgId(orgId);
14
+ if (!app) return false;
15
+ const { clientSecret, ...rest } = app;
16
+ return rest;
17
+ }
18
+
19
+ async createApp(orgId: string, dto: CreateOAuthAppDto) {
20
+ const existing = await this._oauthRepository.getAppByOrgId(orgId);
21
+ if (existing) {
22
+ throw new HttpException(
23
+ 'You can only have one OAuth application per organization',
24
+ HttpStatus.BAD_REQUEST
25
+ );
26
+ }
27
+
28
+ const clientId = 'pca_' + makeId(32);
29
+ const clientSecret = 'pcs_' + makeId(48);
30
+ const encryptedSecret = AuthService.fixedEncryption(clientSecret);
31
+
32
+ const app = await this._oauthRepository.createApp(orgId, {
33
+ name: dto.name,
34
+ description: dto.description,
35
+ pictureId: dto.pictureId,
36
+ redirectUrl: dto.redirectUrl,
37
+ clientId,
38
+ clientSecret: encryptedSecret,
39
+ });
40
+
41
+ return { ...app, clientSecret };
42
+ }
43
+
44
+ async updateApp(orgId: string, dto: UpdateOAuthAppDto) {
45
+ return this._oauthRepository.updateApp(orgId, {
46
+ ...(dto.name && { name: dto.name }),
47
+ ...(dto.description !== undefined && { description: dto.description }),
48
+ ...(dto.pictureId !== undefined && { pictureId: dto.pictureId }),
49
+ ...(dto.redirectUrl && { redirectUrl: dto.redirectUrl }),
50
+ });
51
+ }
52
+
53
+ async deleteApp(orgId: string) {
54
+ const app = await this._oauthRepository.getAppByOrgId(orgId);
55
+ if (!app) {
56
+ throw new HttpException('No OAuth app found', HttpStatus.NOT_FOUND);
57
+ }
58
+ await this._oauthRepository.revokeAllForApp(app.id);
59
+ await this._oauthRepository.deleteApp(orgId);
60
+ return { success: true };
61
+ }
62
+
63
+ async rotateSecret(orgId: string) {
64
+ const app = await this._oauthRepository.getAppByOrgId(orgId);
65
+ if (!app) {
66
+ throw new HttpException('No OAuth app found', HttpStatus.NOT_FOUND);
67
+ }
68
+
69
+ const newSecret = 'pcs_' + makeId(48);
70
+ const encrypted = AuthService.fixedEncryption(newSecret);
71
+ await this._oauthRepository.updateClientSecret(orgId, encrypted);
72
+ return { clientSecret: newSecret };
73
+ }
74
+
75
+ async validateAuthorizationRequest(clientId: string) {
76
+ const app = await this._oauthRepository.getAppByClientId(clientId);
77
+ if (!app) {
78
+ throw new HttpException('Invalid client_id', HttpStatus.BAD_REQUEST);
79
+ }
80
+ return app;
81
+ }
82
+
83
+ async createAuthorizationCode(
84
+ oauthAppId: string,
85
+ userId: string,
86
+ organizationId: string
87
+ ) {
88
+ const code = makeId(32);
89
+ const encryptedCode = AuthService.fixedEncryption(code);
90
+ const codeExpiresAt = new Date(Date.now() + 10 * 60 * 1000);
91
+
92
+ await this._oauthRepository.createAuthorization({
93
+ oauthAppId,
94
+ userId,
95
+ organizationId,
96
+ authorizationCode: encryptedCode,
97
+ codeExpiresAt,
98
+ });
99
+
100
+ return code;
101
+ }
102
+
103
+ async exchangeCodeForToken(
104
+ code: string,
105
+ clientId: string,
106
+ clientSecret: string
107
+ ) {
108
+ const app = await this._oauthRepository.getAppByClientId(clientId);
109
+ if (!app) {
110
+ throw new HttpException(
111
+ { error: 'invalid_client' },
112
+ HttpStatus.UNAUTHORIZED
113
+ );
114
+ }
115
+
116
+ if (app.clientSecret !== AuthService.fixedEncryption(clientSecret)) {
117
+ throw new HttpException(
118
+ { error: 'invalid_client' },
119
+ HttpStatus.UNAUTHORIZED
120
+ );
121
+ }
122
+
123
+ const encryptedCode = AuthService.fixedEncryption(code);
124
+ const auth = await this._oauthRepository.findByCode(encryptedCode);
125
+ if (!auth || auth.oauthAppId !== app.id) {
126
+ throw new HttpException(
127
+ { error: 'invalid_grant' },
128
+ HttpStatus.BAD_REQUEST
129
+ );
130
+ }
131
+
132
+ if (!auth.codeExpiresAt || new Date() > auth.codeExpiresAt) {
133
+ throw new HttpException(
134
+ { error: 'invalid_grant', error_description: 'Code has expired' },
135
+ HttpStatus.BAD_REQUEST
136
+ );
137
+ }
138
+
139
+ const token = 'pos_' + makeId(40);
140
+ const encryptedToken = AuthService.fixedEncryption(token);
141
+ const {
142
+ organizationId,
143
+ organization: { paymentId },
144
+ } = await this._oauthRepository.exchangeCodeForToken(
145
+ auth.id,
146
+ encryptedToken
147
+ );
148
+
149
+ return {
150
+ id: organizationId,
151
+ cus: paymentId,
152
+ access_token: token,
153
+ token_type: 'bearer',
154
+ };
155
+ }
156
+
157
+ async getOrgByOAuthToken(token: string) {
158
+ const encrypted = AuthService.fixedEncryption(token);
159
+ return this._oauthRepository.findByAccessToken(encrypted);
160
+ }
161
+
162
+ async getApprovedApps(userId: string) {
163
+ return this._oauthRepository.getApprovedApps(userId);
164
+ }
165
+
166
+ async revokeApp(userId: string, authId: string) {
167
+ await this._oauthRepository.revokeAuthorization(userId, authId);
168
+ return { success: true };
169
+ }
170
+ }
libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Role, ShortLinkPreference, SubscriptionTier } from '@prisma/client';
3
+ import { Injectable } from '@nestjs/common';
4
+ import { AuthService } from '@gitroom/helpers/auth/auth.service';
5
+ import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
6
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
7
+
8
+ @Injectable()
9
+ export class OrganizationRepository {
10
+ constructor(
11
+ private _organization: PrismaRepository<'organization'>,
12
+ private _userOrg: PrismaRepository<'userOrganization'>,
13
+ private _user: PrismaRepository<'user'>
14
+ ) {}
15
+
16
+ createMaxUser(id: string, name: string, saasName: string, email: string) {
17
+ return this._organization.model.organization.create({
18
+ select: {
19
+ id: true,
20
+ apiKey: true,
21
+ },
22
+ data: {
23
+ name: name ? `${name}###${id}` : `Unnamed User###${id}`,
24
+ apiKey: AuthService.fixedEncryption(makeId(20)),
25
+ isTrailing: false,
26
+ subscription: {
27
+ create: {
28
+ totalChannels: 1000000,
29
+ subscriptionTier: 'ULTIMATE',
30
+ isLifetime: true,
31
+ period: 'YEARLY',
32
+ },
33
+ },
34
+ users: {
35
+ create: {
36
+ role: Role.SUPERADMIN,
37
+ user: {
38
+ create: {
39
+ activated: true,
40
+ email: email
41
+ ? email.split('@').join(`+${saasName}@`)
42
+ : `${saasName}+` + makeId(10) + '@postiz.com',
43
+ name: name ? `${name}###${id}` : `Unnamed User###${id}`,
44
+ providerName: 'LOCAL',
45
+ password: AuthService.hashPassword(makeId(500)),
46
+ timezone: 0,
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ });
53
+ }
54
+
55
+ getOrgByApiKey(api: string) {
56
+ return this._organization.model.organization.findFirst({
57
+ where: {
58
+ apiKey: api,
59
+ },
60
+ include: {
61
+ subscription: {
62
+ select: {
63
+ subscriptionTier: true,
64
+ totalChannels: true,
65
+ isLifetime: true,
66
+ },
67
+ },
68
+ },
69
+ });
70
+ }
71
+
72
+ getCount() {
73
+ return this._organization.model.organization.count();
74
+ }
75
+
76
+ getUserOrg(id: string) {
77
+ return this._userOrg.model.userOrganization.findFirst({
78
+ where: {
79
+ id,
80
+ },
81
+ select: {
82
+ user: true,
83
+ organization: {
84
+ include: {
85
+ users: {
86
+ select: {
87
+ id: true,
88
+ disabled: true,
89
+ role: true,
90
+ userId: true,
91
+ },
92
+ },
93
+ subscription: {
94
+ select: {
95
+ subscriptionTier: true,
96
+ totalChannels: true,
97
+ isLifetime: true,
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ });
104
+ }
105
+
106
+ getImpersonateUser(name: string) {
107
+ return this._userOrg.model.userOrganization.findMany({
108
+ where: {
109
+ OR: [
110
+ {
111
+ organizationId: {
112
+ contains: name,
113
+ },
114
+ },
115
+ {
116
+ user: {
117
+ OR: [
118
+ {
119
+ name: {
120
+ contains: name,
121
+ },
122
+ },
123
+ {
124
+ email: {
125
+ contains: name,
126
+ },
127
+ },
128
+ {
129
+ id: {
130
+ contains: name,
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ },
136
+ ],
137
+ },
138
+ select: {
139
+ id: true,
140
+ organization: {
141
+ select: {
142
+ id: true,
143
+ },
144
+ },
145
+ user: {
146
+ select: {
147
+ id: true,
148
+ name: true,
149
+ email: true,
150
+ },
151
+ },
152
+ },
153
+ });
154
+ }
155
+
156
+ updateApiKey(orgId: string) {
157
+ return this._organization.model.organization.update({
158
+ where: {
159
+ id: orgId,
160
+ },
161
+ data: {
162
+ apiKey: AuthService.fixedEncryption(makeId(20)),
163
+ },
164
+ });
165
+ }
166
+
167
+ async getOrgsByUserId(userId: string) {
168
+ return this._organization.model.organization.findMany({
169
+ where: {
170
+ users: {
171
+ some: {
172
+ userId,
173
+ },
174
+ },
175
+ },
176
+ include: {
177
+ users: {
178
+ where: {
179
+ userId,
180
+ },
181
+ select: {
182
+ disabled: true,
183
+ role: true,
184
+ },
185
+ },
186
+ subscription: {
187
+ select: {
188
+ subscriptionTier: true,
189
+ totalChannels: true,
190
+ isLifetime: true,
191
+ createdAt: true,
192
+ },
193
+ },
194
+ },
195
+ });
196
+ }
197
+
198
+ async getOrgById(id: string) {
199
+ return this._organization.model.organization.findUnique({
200
+ where: {
201
+ id,
202
+ },
203
+ });
204
+ }
205
+
206
+ async addUserToOrg(
207
+ userId: string,
208
+ id: string,
209
+ orgId: string,
210
+ role: 'USER' | 'ADMIN'
211
+ ) {
212
+ const checkIfInviteExists = await this._user.model.user.findFirst({
213
+ where: {
214
+ inviteId: id,
215
+ },
216
+ });
217
+
218
+ if (checkIfInviteExists) {
219
+ return false;
220
+ }
221
+
222
+ const checkForSubscription =
223
+ await this._organization.model.organization.findFirst({
224
+ where: {
225
+ id: orgId,
226
+ },
227
+ select: {
228
+ subscription: true,
229
+ },
230
+ });
231
+
232
+ if (
233
+ process.env.STRIPE_PUBLISHABLE_KEY &&
234
+ checkForSubscription?.subscription?.subscriptionTier ===
235
+ SubscriptionTier.STANDARD
236
+ ) {
237
+ return false;
238
+ }
239
+
240
+ const create = await this._userOrg.model.userOrganization.create({
241
+ data: {
242
+ role,
243
+ userId,
244
+ organizationId: orgId,
245
+ },
246
+ });
247
+
248
+ await this._user.model.user.update({
249
+ where: {
250
+ id: userId,
251
+ },
252
+ data: {
253
+ inviteId: id,
254
+ },
255
+ });
256
+
257
+ return create;
258
+ }
259
+
260
+ async createOrgAndUser(
261
+ body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
262
+ hasEmail: boolean,
263
+ ip: string,
264
+ userAgent: string
265
+ ) {
266
+ return this._organization.model.organization.create({
267
+ data: {
268
+ name: body.company,
269
+ apiKey: AuthService.fixedEncryption(makeId(20)),
270
+ allowTrial: true,
271
+ isTrailing: true,
272
+ users: {
273
+ create: {
274
+ role: Role.SUPERADMIN,
275
+ user: {
276
+ create: {
277
+ activated: body.provider !== 'LOCAL' || !hasEmail,
278
+ email: body.email,
279
+ password: body.password
280
+ ? AuthService.hashPassword(body.password)
281
+ : '',
282
+ providerName: body.provider,
283
+ providerId: body.providerId || '',
284
+ timezone: 0,
285
+ ip,
286
+ agent: userAgent,
287
+ },
288
+ },
289
+ },
290
+ },
291
+ },
292
+ select: {
293
+ id: true,
294
+ users: {
295
+ select: {
296
+ user: true,
297
+ },
298
+ },
299
+ },
300
+ });
301
+ }
302
+
303
+ getOrgByCustomerId(customerId: string) {
304
+ return this._organization.model.organization.findFirst({
305
+ where: {
306
+ paymentId: customerId,
307
+ },
308
+ });
309
+ }
310
+
311
+ async setStreak(organizationId: string, type: 'start' | 'end') {
312
+ try {
313
+ await this._organization.model.organization.update({
314
+ where: {
315
+ id: organizationId,
316
+ ...(type === 'start'
317
+ ? {
318
+ streakSince: null,
319
+ }
320
+ : {}),
321
+ },
322
+ data: {
323
+ ...(type === 'end' ? { streakSince: null } : {}),
324
+ ...(type === 'start' ? { streakSince: new Date() } : {}),
325
+ },
326
+ });
327
+ } catch (err) {}
328
+ }
329
+
330
+ async getTeam(orgId: string) {
331
+ return this._organization.model.organization.findUnique({
332
+ where: {
333
+ id: orgId,
334
+ },
335
+ select: {
336
+ users: {
337
+ select: {
338
+ role: true,
339
+ user: {
340
+ select: {
341
+ email: true,
342
+ id: true,
343
+ sendSuccessEmails: true,
344
+ sendFailureEmails: true,
345
+ sendStreakEmails: true,
346
+ },
347
+ },
348
+ },
349
+ },
350
+ },
351
+ });
352
+ }
353
+
354
+ getAllUsersOrgs(orgId: string) {
355
+ return this._organization.model.organization.findUnique({
356
+ where: {
357
+ id: orgId,
358
+ },
359
+ select: {
360
+ users: {
361
+ select: {
362
+ user: {
363
+ select: {
364
+ email: true,
365
+ id: true,
366
+ sendSuccessEmails: true,
367
+ sendFailureEmails: true,
368
+ },
369
+ },
370
+ },
371
+ },
372
+ },
373
+ });
374
+ }
375
+
376
+ async deleteTeamMember(orgId: string, userId: string) {
377
+ return this._userOrg.model.userOrganization.delete({
378
+ where: {
379
+ userId_organizationId: {
380
+ userId,
381
+ organizationId: orgId,
382
+ },
383
+ },
384
+ });
385
+ }
386
+
387
+ disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) {
388
+ return this._userOrg.model.userOrganization.updateMany({
389
+ where: {
390
+ organizationId: orgId,
391
+ role: {
392
+ not: Role.SUPERADMIN,
393
+ },
394
+ },
395
+ data: {
396
+ disabled: disable,
397
+ },
398
+ });
399
+ }
400
+
401
+ getShortlinkPreference(orgId: string) {
402
+ return this._organization.model.organization.findUnique({
403
+ where: {
404
+ id: orgId,
405
+ },
406
+ select: {
407
+ shortlink: true,
408
+ },
409
+ });
410
+ }
411
+
412
+ updateShortlinkPreference(orgId: string, shortlink: ShortLinkPreference) {
413
+ return this._organization.model.organization.update({
414
+ where: {
415
+ id: orgId,
416
+ },
417
+ data: {
418
+ shortlink,
419
+ },
420
+ });
421
+ }
422
+ }
libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
4
+ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
5
+ import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto';
6
+ import { AuthService } from '@gitroom/helpers/auth/auth.service';
7
+ import dayjs from 'dayjs';
8
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
9
+ import { Organization, ShortLinkPreference } from '@prisma/client';
10
+ import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
11
+
12
+ @Injectable()
13
+ export class OrganizationService {
14
+ constructor(
15
+ private _organizationRepository: OrganizationRepository,
16
+ private _notificationsService: NotificationService
17
+ ) {}
18
+ async createOrgAndUser(
19
+ body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string },
20
+ ip: string,
21
+ userAgent: string
22
+ ) {
23
+ return this._organizationRepository.createOrgAndUser(
24
+ body,
25
+ this._notificationsService.hasEmailProvider(),
26
+ ip,
27
+ userAgent
28
+ );
29
+ }
30
+
31
+ async getCount() {
32
+ return this._organizationRepository.getCount();
33
+ }
34
+
35
+ async createMaxUser(id: string, name: string, saasName: string, email: string) {
36
+ return this._organizationRepository.createMaxUser(id, name, saasName, email);
37
+ }
38
+
39
+ addUserToOrg(
40
+ userId: string,
41
+ id: string,
42
+ orgId: string,
43
+ role: 'USER' | 'ADMIN'
44
+ ) {
45
+ return this._organizationRepository.addUserToOrg(userId, id, orgId, role);
46
+ }
47
+
48
+ getOrgById(id: string) {
49
+ return this._organizationRepository.getOrgById(id);
50
+ }
51
+
52
+ getOrgByApiKey(api: string) {
53
+ return this._organizationRepository.getOrgByApiKey(api);
54
+ }
55
+
56
+ getUserOrg(id: string) {
57
+ return this._organizationRepository.getUserOrg(id);
58
+ }
59
+
60
+ getOrgsByUserId(userId: string) {
61
+ return this._organizationRepository.getOrgsByUserId(userId);
62
+ }
63
+
64
+ updateApiKey(orgId: string) {
65
+ return this._organizationRepository.updateApiKey(orgId);
66
+ }
67
+
68
+ getTeam(orgId: string) {
69
+ return this._organizationRepository.getTeam(orgId);
70
+ }
71
+
72
+ async setStreak(organizationId: string, type: 'start' | 'end') {
73
+ return this._organizationRepository.setStreak(organizationId, type);
74
+ }
75
+
76
+ getOrgByCustomerId(customerId: string) {
77
+ return this._organizationRepository.getOrgByCustomerId(customerId);
78
+ }
79
+
80
+ async inviteTeamMember(orgId: string, body: AddTeamMemberDto) {
81
+ const timeLimit = dayjs().add(2, 'day').format('YYYY-MM-DD HH:mm:ss');
82
+ const id = makeId(5);
83
+ const url =
84
+ process.env.FRONTEND_URL +
85
+ `/?org=${AuthService.signJWT({ ...body, orgId, timeLimit, id })}`;
86
+ if (body.sendEmail) {
87
+ await this._notificationsService.sendEmail(
88
+ body.email,
89
+ 'You have been invited to join an organization',
90
+ `You have been invited to join an organization. Click <a href="${url}">here</a> to join.<br />The link will expire in 2 days.`
91
+ );
92
+ }
93
+ return { url };
94
+ }
95
+
96
+ async deleteTeamMember(org: Organization, userId: string) {
97
+ const userOrgs = await this._organizationRepository.getOrgsByUserId(userId);
98
+ const findOrgToDelete = userOrgs.find((orgUser) => orgUser.id === org.id);
99
+ if (!findOrgToDelete) {
100
+ throw new Error('User is not part of this organization');
101
+ }
102
+
103
+ // @ts-ignore
104
+ const myRole = org.users[0].role;
105
+ const userRole = findOrgToDelete.users[0].role;
106
+ const myLevel = myRole === 'USER' ? 0 : myRole === 'ADMIN' ? 1 : 2;
107
+ const userLevel = userRole === 'USER' ? 0 : userRole === 'ADMIN' ? 1 : 2;
108
+
109
+ if (myLevel < userLevel) {
110
+ throw new Error('You do not have permission to delete this user');
111
+ }
112
+
113
+ return this._organizationRepository.deleteTeamMember(org.id, userId);
114
+ }
115
+
116
+ disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) {
117
+ return this._organizationRepository.disableOrEnableNonSuperAdminUsers(
118
+ orgId,
119
+ disable
120
+ );
121
+ }
122
+
123
+ getShortlinkPreference(orgId: string) {
124
+ return this._organizationRepository.getShortlinkPreference(orgId);
125
+ }
126
+
127
+ updateShortlinkPreference(orgId: string, shortlink: ShortLinkPreference) {
128
+ return this._organizationRepository.updateShortlinkPreference(
129
+ orgId,
130
+ shortlink
131
+ );
132
+ }
133
+ }
libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts ADDED
@@ -0,0 +1,923 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { Post as PostBody } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
4
+ import {
5
+ APPROVED_SUBMIT_FOR_ORDER,
6
+ CreationMethod,
7
+ Post,
8
+ State,
9
+ } from '@prisma/client';
10
+ import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
11
+ import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
12
+ import dayjs from 'dayjs';
13
+ import isoWeek from 'dayjs/plugin/isoWeek';
14
+ import weekOfYear from 'dayjs/plugin/weekOfYear';
15
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
16
+ import utc from 'dayjs/plugin/utc';
17
+ import { v4 as uuidv4 } from 'uuid';
18
+ import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
19
+
20
+ dayjs.extend(isoWeek);
21
+ dayjs.extend(weekOfYear);
22
+ dayjs.extend(isSameOrAfter);
23
+ dayjs.extend(utc);
24
+
25
+ @Injectable()
26
+ export class PostsRepository {
27
+ constructor(
28
+ private _post: PrismaRepository<'post'>,
29
+ private _popularPosts: PrismaRepository<'popularPosts'>,
30
+ private _comments: PrismaRepository<'comments'>,
31
+ private _tags: PrismaRepository<'tags'>,
32
+ private _tagsPosts: PrismaRepository<'tagsPosts'>,
33
+ private _errors: PrismaRepository<'errors'>
34
+ ) {}
35
+
36
+ searchForMissingThreeHoursPosts() {
37
+ return this._post.model.post.findMany({
38
+ where: {
39
+ integration: {
40
+ refreshNeeded: false,
41
+ inBetweenSteps: false,
42
+ disabled: false,
43
+ deletedAt: null,
44
+ },
45
+ publishDate: {
46
+ gte: dayjs.utc().subtract(2, 'day').toDate(),
47
+ lt: dayjs.utc().toDate(),
48
+ },
49
+ state: 'QUEUE',
50
+ deletedAt: null,
51
+ parentPostId: null,
52
+ },
53
+ select: {
54
+ id: true,
55
+ organizationId: true,
56
+ integration: {
57
+ select: {
58
+ providerIdentifier: true,
59
+ },
60
+ },
61
+ publishDate: true,
62
+ },
63
+ });
64
+ }
65
+
66
+ getOldPosts(orgId: string, date: string) {
67
+ return this._post.model.post.findMany({
68
+ where: {
69
+ integration: {
70
+ refreshNeeded: false,
71
+ inBetweenSteps: false,
72
+ disabled: false,
73
+ },
74
+ organizationId: orgId,
75
+ publishDate: {
76
+ lte: dayjs(date).toDate(),
77
+ },
78
+ deletedAt: null,
79
+ parentPostId: null,
80
+ },
81
+ orderBy: {
82
+ publishDate: 'desc',
83
+ },
84
+ select: {
85
+ id: true,
86
+ content: true,
87
+ publishDate: true,
88
+ releaseURL: true,
89
+ state: true,
90
+ integration: {
91
+ select: {
92
+ id: true,
93
+ name: true,
94
+ providerIdentifier: true,
95
+ picture: true,
96
+ type: true,
97
+ },
98
+ },
99
+ },
100
+ });
101
+ }
102
+
103
+ updateImages(id: string, images: string) {
104
+ return this._post.model.post.update({
105
+ where: {
106
+ id,
107
+ },
108
+ data: {
109
+ image: images,
110
+ },
111
+ });
112
+ }
113
+
114
+ getPostUrls(orgId: string, ids: string[]) {
115
+ return this._post.model.post.findMany({
116
+ where: {
117
+ organizationId: orgId,
118
+ id: {
119
+ in: ids,
120
+ },
121
+ },
122
+ select: {
123
+ id: true,
124
+ releaseURL: true,
125
+ },
126
+ });
127
+ }
128
+
129
+ async getPosts(orgId: string, query: GetPostsDto) {
130
+ // Use the provided start and end dates directly
131
+ const startDate = dayjs.utc(query.startDate).toDate();
132
+ const endDate = dayjs.utc(query.endDate).toDate();
133
+
134
+ const list = await this._post.model.post.findMany({
135
+ where: {
136
+ AND: [
137
+ {
138
+ OR: [
139
+ {
140
+ organizationId: orgId,
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ OR: [
146
+ {
147
+ publishDate: {
148
+ gte: startDate,
149
+ lte: endDate,
150
+ },
151
+ },
152
+ {
153
+ intervalInDays: {
154
+ not: null,
155
+ },
156
+ },
157
+ ],
158
+ },
159
+ ],
160
+ integration: {
161
+ deletedAt: null,
162
+ organizationId: orgId,
163
+ },
164
+ deletedAt: null,
165
+ parentPostId: null,
166
+ ...(query.customer
167
+ ? {
168
+ integration: {
169
+ customerId: query.customer,
170
+ },
171
+ }
172
+ : {}),
173
+ },
174
+ select: {
175
+ id: true,
176
+ content: true,
177
+ publishDate: true,
178
+ releaseURL: true,
179
+ releaseId: true,
180
+ state: true,
181
+ intervalInDays: true,
182
+ group: true,
183
+ creationMethod: true,
184
+ tags: {
185
+ select: {
186
+ tag: true,
187
+ },
188
+ },
189
+ integration: {
190
+ select: {
191
+ id: true,
192
+ providerIdentifier: true,
193
+ name: true,
194
+ picture: true,
195
+ },
196
+ },
197
+ },
198
+ });
199
+
200
+ return list.reduce((all, post) => {
201
+ if (!post.intervalInDays) {
202
+ return [...all, post];
203
+ }
204
+
205
+ const addMorePosts = [];
206
+ let startingDate = dayjs.utc(post.publishDate);
207
+ while (dayjs.utc(endDate).isSameOrAfter(startingDate)) {
208
+ if (dayjs(startingDate).isSameOrAfter(dayjs.utc(post.publishDate))) {
209
+ addMorePosts.push({
210
+ ...post,
211
+ publishDate: startingDate.toDate(),
212
+ actualDate: post.publishDate,
213
+ });
214
+ }
215
+
216
+ startingDate = startingDate.add(post.intervalInDays, 'days');
217
+ }
218
+
219
+ return [...all, ...addMorePosts];
220
+ }, [] as any[]);
221
+ }
222
+
223
+ async getPostsList(orgId: string, query: GetPostsListDto) {
224
+ const page = query.page || 0;
225
+ const limit = query.limit || 20;
226
+ const skip = page * limit;
227
+
228
+ const stateFilter = query.state || 'all';
229
+ const stateAndDate =
230
+ stateFilter === 'scheduled'
231
+ ? {
232
+ state: State.QUEUE,
233
+ }
234
+ : stateFilter === 'draft'
235
+ ? { state: State.DRAFT }
236
+ : stateFilter === 'published'
237
+ ? { state: State.PUBLISHED }
238
+ : {
239
+ state: {
240
+ in: [State.QUEUE, State.DRAFT, State.PUBLISHED, State.ERROR],
241
+ },
242
+ };
243
+
244
+ const orderDirection: 'asc' | 'desc' =
245
+ stateFilter === 'published' ? 'desc' : 'asc';
246
+
247
+ const where = {
248
+ AND: [
249
+ {
250
+ OR: [
251
+ {
252
+ organizationId: orgId,
253
+ },
254
+ ],
255
+ },
256
+ ],
257
+ ...stateAndDate,
258
+ // Published posts were already posted (publishDate in the past), so fetch
259
+ // all of them; everything else stays upcoming. Ordering handles the rest.
260
+ ...(stateFilter === 'published'
261
+ ? {}
262
+ : { publishDate: { gte: dayjs.utc().toDate() } }),
263
+ deletedAt: null as Date | null,
264
+ parentPostId: null as string | null,
265
+ intervalInDays: null as number | null,
266
+
267
+ integration: {
268
+ deletedAt: null as any,
269
+ organizationId: orgId,
270
+ ...(query.customer
271
+ ? {
272
+ customerId: query.customer,
273
+ }
274
+ : {}),
275
+ },
276
+ };
277
+
278
+ const [posts, total] = await Promise.all([
279
+ this._post.model.post.findMany({
280
+ where,
281
+ skip,
282
+ take: limit,
283
+ orderBy: {
284
+ publishDate: orderDirection,
285
+ },
286
+ select: {
287
+ id: true,
288
+ content: true,
289
+ publishDate: true,
290
+ releaseURL: true,
291
+ releaseId: true,
292
+ state: true,
293
+ intervalInDays: true,
294
+ group: true,
295
+ creationMethod: true,
296
+ tags: {
297
+ select: {
298
+ tag: true,
299
+ },
300
+ },
301
+ integration: {
302
+ select: {
303
+ id: true,
304
+ providerIdentifier: true,
305
+ name: true,
306
+ picture: true,
307
+ },
308
+ },
309
+ },
310
+ }),
311
+ this._post.model.post.count({ where }),
312
+ ]);
313
+
314
+ return {
315
+ posts,
316
+ total,
317
+ page,
318
+ limit,
319
+ hasMore: skip + posts.length < total,
320
+ };
321
+ }
322
+
323
+ async deletePost(orgId: string, group: string) {
324
+ await this._post.model.post.updateMany({
325
+ where: {
326
+ organizationId: orgId,
327
+ group,
328
+ },
329
+ data: {
330
+ deletedAt: new Date(),
331
+ },
332
+ });
333
+
334
+ return this._post.model.post.findFirst({
335
+ where: {
336
+ organizationId: orgId,
337
+ group,
338
+ parentPostId: null,
339
+ },
340
+ select: {
341
+ id: true,
342
+ },
343
+ });
344
+ }
345
+
346
+ getPostsByGroup(orgId: string, group: string) {
347
+ return this._post.model.post.findMany({
348
+ where: {
349
+ group,
350
+ ...(orgId ? { organizationId: orgId } : {}),
351
+ deletedAt: null,
352
+ },
353
+ include: {
354
+ integration: true,
355
+ tags: {
356
+ select: {
357
+ tag: true,
358
+ },
359
+ },
360
+ },
361
+ });
362
+ }
363
+
364
+ getPost(
365
+ id: string,
366
+ includeIntegration = false,
367
+ orgId?: string,
368
+ isFirst?: boolean
369
+ ) {
370
+ return this._post.model.post.findUnique({
371
+ where: {
372
+ id,
373
+ ...(orgId ? { organizationId: orgId } : {}),
374
+ deletedAt: null,
375
+ },
376
+ include: {
377
+ ...(includeIntegration
378
+ ? {
379
+ integration: true,
380
+ tags: {
381
+ select: {
382
+ tag: true,
383
+ },
384
+ },
385
+ }
386
+ : {}),
387
+ childrenPost: true,
388
+ },
389
+ });
390
+ }
391
+
392
+ updatePost(id: string, postId: string, releaseURL: string) {
393
+ return this._post.model.post.update({
394
+ where: {
395
+ id,
396
+ },
397
+ data: {
398
+ state: 'PUBLISHED',
399
+ releaseURL,
400
+ releaseId: postId,
401
+ },
402
+ });
403
+ }
404
+
405
+ updateReleaseId(id: string, orgId: string, releaseId: string) {
406
+ return this._post.model.post.update({
407
+ where: {
408
+ id,
409
+ organizationId: orgId,
410
+ releaseId: 'missing',
411
+ },
412
+ data: {
413
+ releaseId: String(releaseId),
414
+ },
415
+ });
416
+ }
417
+
418
+ async changeState(id: string, state: State, err?: any, body?: any) {
419
+ const update = await this._post.model.post.update({
420
+ where: {
421
+ id,
422
+ },
423
+ data: {
424
+ state,
425
+ ...(err
426
+ ? { error: typeof err === 'string' ? err : JSON.stringify(err) }
427
+ : {}),
428
+ },
429
+ include: {
430
+ integration: {
431
+ select: {
432
+ providerIdentifier: true,
433
+ },
434
+ },
435
+ },
436
+ });
437
+
438
+ if (state === 'ERROR' && err && body) {
439
+ try {
440
+ await this._errors.model.errors.create({
441
+ data: {
442
+ message: typeof err === 'string' ? err : JSON.stringify(err),
443
+ organizationId: update.organizationId,
444
+ platform: update.integration.providerIdentifier,
445
+ postId: update.id,
446
+ body: typeof body === 'string' ? body : JSON.stringify(body),
447
+ },
448
+ });
449
+ } catch (err) {}
450
+ }
451
+
452
+ return update;
453
+ }
454
+
455
+ getErrorsByPostIds(postIds: string[]) {
456
+ return this._errors.model.errors.findMany({
457
+ where: {
458
+ postId: { in: postIds },
459
+ },
460
+ orderBy: { createdAt: 'desc' },
461
+ });
462
+ }
463
+
464
+ async changeDate(
465
+ orgId: string,
466
+ id: string,
467
+ date: string,
468
+ isDraft: boolean,
469
+ action: 'schedule' | 'update' = 'schedule'
470
+ ) {
471
+ return this._post.model.post.update({
472
+ where: {
473
+ organizationId: orgId,
474
+ id,
475
+ },
476
+ data: {
477
+ publishDate: dayjs(date).toDate(),
478
+ // schedule: set state to QUEUE (or DRAFT if it was a draft)
479
+ // update: don't change the state
480
+ ...(action === 'schedule'
481
+ ? {
482
+ state: isDraft ? 'DRAFT' : 'QUEUE',
483
+ releaseId: null,
484
+ releaseURL: null,
485
+ }
486
+ : {}),
487
+ },
488
+ });
489
+ }
490
+
491
+ countPostsFromDay(orgId: string, date: Date) {
492
+ return this._post.model.post.count({
493
+ where: {
494
+ organizationId: orgId,
495
+ publishDate: {
496
+ gte: date,
497
+ },
498
+ OR: [
499
+ {
500
+ deletedAt: null,
501
+ state: {
502
+ in: ['QUEUE'],
503
+ },
504
+ },
505
+ {
506
+ state: 'PUBLISHED',
507
+ },
508
+ ],
509
+ },
510
+ });
511
+ }
512
+
513
+ async createOrUpdatePost(
514
+ state: 'draft' | 'schedule' | 'now' | 'update',
515
+ orgId: string,
516
+ date: string,
517
+ body: PostBody,
518
+ tags: { value: string; label: string }[],
519
+ creationMethod: CreationMethod,
520
+ inter?: number
521
+ ) {
522
+ const posts: Post[] = [];
523
+ const uuid = uuidv4();
524
+
525
+ for (const value of body.value) {
526
+ const updateData = (type: 'create' | 'update') => ({
527
+ publishDate: dayjs(date).toDate(),
528
+ integration: {
529
+ connect: {
530
+ id: body.integration.id,
531
+ organizationId: orgId,
532
+ },
533
+ },
534
+ ...(posts?.[posts.length - 1]?.id
535
+ ? {
536
+ parentPost: {
537
+ connect: {
538
+ id: posts[posts.length - 1]?.id,
539
+ },
540
+ },
541
+ }
542
+ : type === 'update'
543
+ ? {
544
+ parentPost: {
545
+ disconnect: true,
546
+ },
547
+ }
548
+ : {}),
549
+ content: value.content,
550
+ delay: value.delay || 0,
551
+ group: uuid,
552
+ intervalInDays: inter ? +inter : null,
553
+ approvedSubmitForOrder: APPROVED_SUBMIT_FOR_ORDER.NO,
554
+ ...(type === 'create' ? { creationMethod } : {}),
555
+ ...(state === 'update'
556
+ ? {}
557
+ : {
558
+ state:
559
+ state === 'draft' ? ('DRAFT' as const) : ('QUEUE' as const),
560
+ }),
561
+ image: JSON.stringify(value.image),
562
+ settings: JSON.stringify(body.settings),
563
+ organization: {
564
+ connect: {
565
+ id: orgId,
566
+ },
567
+ },
568
+ });
569
+
570
+ posts.push(
571
+ await this._post.model.post.upsert({
572
+ where: {
573
+ id: value.id || uuidv4(),
574
+ },
575
+ create: { ...updateData('create') },
576
+ update: {
577
+ ...updateData('update'),
578
+ lastMessage: {
579
+ disconnect: true,
580
+ },
581
+ submittedForOrder: {
582
+ disconnect: true,
583
+ },
584
+ },
585
+ })
586
+ );
587
+
588
+ if (posts.length === 1) {
589
+ await this._tagsPosts.model.tagsPosts.deleteMany({
590
+ where: {
591
+ post: {
592
+ id: posts[0].id,
593
+ },
594
+ },
595
+ });
596
+
597
+ if (tags.length) {
598
+ const tagsList = await this._tags.model.tags.findMany({
599
+ where: {
600
+ orgId: orgId,
601
+ name: {
602
+ in: tags.map((tag) => tag.label).filter((f) => f),
603
+ },
604
+ },
605
+ });
606
+
607
+ if (tagsList.length) {
608
+ await this._post.model.post.update({
609
+ where: {
610
+ id: posts[posts.length - 1].id,
611
+ },
612
+ data: {
613
+ tags: {
614
+ createMany: {
615
+ data: tagsList.map((tag) => ({
616
+ tagId: tag.id,
617
+ })),
618
+ },
619
+ },
620
+ },
621
+ });
622
+ }
623
+ }
624
+ }
625
+ }
626
+
627
+ const previousPost = body.group
628
+ ? (
629
+ await this._post.model.post.findFirst({
630
+ where: {
631
+ group: body.group,
632
+ deletedAt: null,
633
+ parentPostId: null,
634
+ },
635
+ select: {
636
+ id: true,
637
+ },
638
+ })
639
+ )?.id!
640
+ : undefined;
641
+
642
+ if (body.group) {
643
+ await this._post.model.post.updateMany({
644
+ where: {
645
+ group: body.group,
646
+ deletedAt: null,
647
+ },
648
+ data: {
649
+ parentPostId: null,
650
+ deletedAt: new Date(),
651
+ },
652
+ });
653
+ }
654
+
655
+ return { previousPost, posts };
656
+ }
657
+
658
+ async submit(id: string, order: string, buyerOrganizationId: string) {
659
+ return this._post.model.post.update({
660
+ where: {
661
+ id,
662
+ },
663
+ data: {
664
+ submittedForOrderId: order,
665
+ approvedSubmitForOrder: 'WAITING_CONFIRMATION',
666
+ submittedForOrganizationId: buyerOrganizationId,
667
+ },
668
+ select: {
669
+ id: true,
670
+ description: true,
671
+ submittedForOrder: {
672
+ select: {
673
+ messageGroupId: true,
674
+ },
675
+ },
676
+ },
677
+ });
678
+ }
679
+
680
+ updateMessage(id: string, messageId: string) {
681
+ return this._post.model.post.update({
682
+ where: {
683
+ id,
684
+ },
685
+ data: {
686
+ lastMessageId: messageId,
687
+ },
688
+ });
689
+ }
690
+
691
+ getPostById(id: string, org?: string) {
692
+ return this._post.model.post.findUnique({
693
+ where: {
694
+ id,
695
+ ...(org ? { organizationId: org } : {}),
696
+ },
697
+ include: {
698
+ integration: true,
699
+ submittedForOrder: {
700
+ include: {
701
+ posts: {
702
+ where: {
703
+ state: 'PUBLISHED',
704
+ },
705
+ },
706
+ ordersItems: true,
707
+ seller: {
708
+ select: {
709
+ id: true,
710
+ account: true,
711
+ },
712
+ },
713
+ },
714
+ },
715
+ },
716
+ });
717
+ }
718
+
719
+ findAllExistingCategories() {
720
+ return this._popularPosts.model.popularPosts.findMany({
721
+ select: {
722
+ category: true,
723
+ },
724
+ distinct: ['category'],
725
+ });
726
+ }
727
+
728
+ findAllExistingTopicsOfCategory(category: string) {
729
+ return this._popularPosts.model.popularPosts.findMany({
730
+ where: {
731
+ category,
732
+ },
733
+ select: {
734
+ topic: true,
735
+ },
736
+ distinct: ['topic'],
737
+ });
738
+ }
739
+
740
+ findPopularPosts(category: string, topic?: string) {
741
+ return this._popularPosts.model.popularPosts.findMany({
742
+ where: {
743
+ category,
744
+ ...(topic ? { topic } : {}),
745
+ },
746
+ select: {
747
+ content: true,
748
+ hook: true,
749
+ },
750
+ });
751
+ }
752
+
753
+ createPopularPosts(post: {
754
+ category: string;
755
+ topic: string;
756
+ content: string;
757
+ hook: string;
758
+ }) {
759
+ return this._popularPosts.model.popularPosts.create({
760
+ data: {
761
+ category: 'category',
762
+ topic: 'topic',
763
+ content: 'content',
764
+ hook: 'hook',
765
+ },
766
+ });
767
+ }
768
+
769
+ async getPostsCountsByDates(
770
+ orgId: string,
771
+ times: number[],
772
+ date: dayjs.Dayjs
773
+ ) {
774
+ const dates = await this._post.model.post.findMany({
775
+ where: {
776
+ deletedAt: null,
777
+ organizationId: orgId,
778
+ publishDate: {
779
+ in: times.map((time) => {
780
+ return date.clone().add(time, 'minutes').toDate();
781
+ }),
782
+ },
783
+ },
784
+ });
785
+
786
+ return times.filter(
787
+ (time) =>
788
+ date.clone().add(time, 'minutes').isAfter(dayjs.utc()) &&
789
+ !dates.find((dateFind) => {
790
+ return (
791
+ dayjs
792
+ .utc(dateFind.publishDate)
793
+ .diff(date.clone().startOf('day'), 'minutes') == time
794
+ );
795
+ })
796
+ );
797
+ }
798
+
799
+ async getComments(postId: string) {
800
+ return this._comments.model.comments.findMany({
801
+ where: {
802
+ postId,
803
+ },
804
+ orderBy: {
805
+ createdAt: 'asc',
806
+ },
807
+ });
808
+ }
809
+
810
+ async getTags(orgId: string) {
811
+ return this._tags.model.tags.findMany({
812
+ where: {
813
+ orgId,
814
+ deletedAt: null,
815
+ },
816
+ });
817
+ }
818
+
819
+ createTag(orgId: string, body: CreateTagDto) {
820
+ return this._tags.model.tags.create({
821
+ data: {
822
+ orgId,
823
+ name: body.name,
824
+ color: body.color,
825
+ },
826
+ });
827
+ }
828
+
829
+ editTag(id: string, orgId: string, body: CreateTagDto) {
830
+ return this._tags.model.tags.update({
831
+ where: {
832
+ id,
833
+ },
834
+ data: {
835
+ name: body.name,
836
+ color: body.color,
837
+ },
838
+ });
839
+ }
840
+
841
+ deleteTag(id: string, orgId: string) {
842
+ return this._tags.model.tags.update({
843
+ where: {
844
+ id,
845
+ orgId,
846
+ },
847
+ data: {
848
+ deletedAt: new Date(),
849
+ },
850
+ });
851
+ }
852
+
853
+ createComment(
854
+ orgId: string,
855
+ userId: string,
856
+ postId: string,
857
+ content: string
858
+ ) {
859
+ return this._comments.model.comments.create({
860
+ data: {
861
+ organizationId: orgId,
862
+ userId,
863
+ postId,
864
+ content,
865
+ },
866
+ });
867
+ }
868
+
869
+ async getPostByForWebhookId(postId: string) {
870
+ return this._post.model.post.findMany({
871
+ where: {
872
+ id: postId,
873
+ deletedAt: null,
874
+ parentPostId: null,
875
+ },
876
+ select: {
877
+ id: true,
878
+ content: true,
879
+ publishDate: true,
880
+ releaseURL: true,
881
+ state: true,
882
+ integration: {
883
+ select: {
884
+ id: true,
885
+ name: true,
886
+ providerIdentifier: true,
887
+ picture: true,
888
+ type: true,
889
+ },
890
+ },
891
+ },
892
+ });
893
+ }
894
+
895
+ async getPostsSince(orgId: string, since: string) {
896
+ return this._post.model.post.findMany({
897
+ where: {
898
+ organizationId: orgId,
899
+ publishDate: {
900
+ gte: new Date(since),
901
+ },
902
+ deletedAt: null,
903
+ parentPostId: null,
904
+ },
905
+ select: {
906
+ id: true,
907
+ content: true,
908
+ publishDate: true,
909
+ releaseURL: true,
910
+ state: true,
911
+ integration: {
912
+ select: {
913
+ id: true,
914
+ name: true,
915
+ providerIdentifier: true,
916
+ picture: true,
917
+ type: true,
918
+ },
919
+ },
920
+ },
921
+ });
922
+ }
923
+ }
libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts ADDED
@@ -0,0 +1,1174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ ValidationPipe,
5
+ } from '@nestjs/common';
6
+ import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
7
+ import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
8
+ import dayjs from 'dayjs';
9
+ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
10
+ import {
11
+ Integration,
12
+ Post,
13
+ Media,
14
+ From,
15
+ CreationMethod,
16
+ State,
17
+ } from '@prisma/client';
18
+ import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
19
+ import { GetPostsListDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.list.dto';
20
+ import { shuffle } from 'lodash';
21
+ import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
22
+ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
23
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
24
+ import utc from 'dayjs/plugin/utc';
25
+ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
26
+ import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
27
+ import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
28
+ import {
29
+ minifyPostsList,
30
+ minifyPosts,
31
+ } from '@gitroom/helpers/utils/posts.list.minify';
32
+ import axios from 'axios';
33
+ import sharp from 'sharp';
34
+ import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
35
+ import { Readable } from 'stream';
36
+ import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
37
+ dayjs.extend(utc);
38
+ import * as Sentry from '@sentry/nestjs';
39
+ import { TemporalService } from 'nestjs-temporal-core';
40
+ import { TypedSearchAttributes } from '@temporalio/common';
41
+ import {
42
+ organizationId,
43
+ postId as postIdSearchParam,
44
+ } from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
45
+ import { AnalyticsData } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
46
+ import { timer } from '@gitroom/helpers/utils/timer';
47
+ import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
48
+ import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
49
+ import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
50
+ import { hasExtension } from '@gitroom/helpers/utils/has.extension';
51
+ import { stripLinks } from '@gitroom/helpers/utils/strip.links';
52
+ import { validate } from 'class-validator';
53
+ import { plainToInstance } from 'class-transformer';
54
+ import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
55
+ import { weightedLength } from '@gitroom/helpers/utils/count.length';
56
+
57
+ type PostWithConditionals = Post & {
58
+ integration?: Integration;
59
+ childrenPost: Post[];
60
+ };
61
+
62
+ @Injectable()
63
+ export class PostsService {
64
+ private storage = UploadFactory.createStorage();
65
+ constructor(
66
+ private _postRepository: PostsRepository,
67
+ private _integrationManager: IntegrationManager,
68
+ private _integrationService: IntegrationService,
69
+ private _mediaService: MediaService,
70
+ private _shortLinkService: ShortLinkService,
71
+ private _openaiService: OpenaiService,
72
+ private _temporalService: TemporalService,
73
+ private _refreshIntegrationService: RefreshIntegrationService
74
+ ) {}
75
+
76
+ searchForMissingThreeHoursPosts() {
77
+ return this._postRepository.searchForMissingThreeHoursPosts();
78
+ }
79
+
80
+ updatePost(id: string, postId: string, releaseURL: string) {
81
+ return this._postRepository.updatePost(id, postId, releaseURL);
82
+ }
83
+
84
+ async getMissingContent(
85
+ orgId: string,
86
+ postId: string,
87
+ forceRefresh = false
88
+ ): Promise<{ id: string; url: string }[]> {
89
+ const post = await this._postRepository.getPostById(postId, orgId);
90
+ if (!post || post.releaseId !== 'missing') {
91
+ return [];
92
+ }
93
+
94
+ const integrationProvider = this._integrationManager.getSocialIntegration(
95
+ post.integration.providerIdentifier
96
+ );
97
+
98
+ if (!integrationProvider.missing) {
99
+ return [];
100
+ }
101
+
102
+ const getIntegration = post.integration!;
103
+
104
+ if (
105
+ dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
106
+ forceRefresh
107
+ ) {
108
+ const data = await this._refreshIntegrationService.refresh(
109
+ getIntegration
110
+ );
111
+ if (!data) {
112
+ return [];
113
+ }
114
+
115
+ const { accessToken } = data;
116
+
117
+ if (accessToken) {
118
+ getIntegration.token = accessToken;
119
+
120
+ if (integrationProvider.refreshWait) {
121
+ await timer(10000);
122
+ }
123
+ } else {
124
+ await this._integrationService.disconnectChannel(orgId, getIntegration);
125
+ return [];
126
+ }
127
+ }
128
+
129
+ try {
130
+ return await integrationProvider.missing(
131
+ getIntegration.internalId,
132
+ getIntegration.token
133
+ );
134
+ } catch (e) {
135
+ console.log(e);
136
+ if (e instanceof RefreshToken) {
137
+ return this.getMissingContent(orgId, postId, true);
138
+ }
139
+ }
140
+
141
+ return [];
142
+ }
143
+
144
+ async getPostById(postId: string, orgId: string) {
145
+ return this._postRepository.getPostById(postId, orgId);
146
+ }
147
+
148
+ async updateReleaseId(orgId: string, postId: string, releaseId: string) {
149
+ return this._postRepository.updateReleaseId(postId, orgId, releaseId);
150
+ }
151
+
152
+ async checkPostAnalytics(
153
+ orgId: string,
154
+ postId: string,
155
+ date: number,
156
+ forceRefresh = false
157
+ ): Promise<AnalyticsData[] | { missing: true }> {
158
+ const post = await this._postRepository.getPostById(postId, orgId);
159
+ if (!post || !post.releaseId) {
160
+ return [];
161
+ }
162
+
163
+ if (post.releaseId === 'missing') {
164
+ return { missing: true };
165
+ }
166
+
167
+ const integrationProvider = this._integrationManager.getSocialIntegration(
168
+ post.integration.providerIdentifier
169
+ );
170
+
171
+ if (!integrationProvider.postAnalytics) {
172
+ return [];
173
+ }
174
+
175
+ const getIntegration = post.integration!;
176
+
177
+ if (
178
+ dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
179
+ forceRefresh
180
+ ) {
181
+ const data = await this._refreshIntegrationService.refresh(
182
+ getIntegration
183
+ );
184
+ if (!data) {
185
+ return [];
186
+ }
187
+
188
+ const { accessToken } = data;
189
+
190
+ if (accessToken) {
191
+ getIntegration.token = accessToken;
192
+
193
+ if (integrationProvider.refreshWait) {
194
+ await timer(10000);
195
+ }
196
+ } else {
197
+ await this._integrationService.disconnectChannel(orgId, getIntegration);
198
+ return [];
199
+ }
200
+ }
201
+
202
+ // const getIntegrationData = await ioRedis.get(
203
+ // `integration:${orgId}:${post.id}:${date}`
204
+ // );
205
+ // if (getIntegrationData) {
206
+ // return JSON.parse(getIntegrationData);
207
+ // }
208
+
209
+ try {
210
+ const loadAnalytics = await integrationProvider.postAnalytics(
211
+ getIntegration.internalId,
212
+ getIntegration.token,
213
+ post.releaseId,
214
+ date
215
+ );
216
+ await ioRedis.set(
217
+ `integration:${orgId}:${post.id}:${date}`,
218
+ JSON.stringify(loadAnalytics),
219
+ 'EX',
220
+ !process.env.NODE_ENV || process.env.NODE_ENV === 'development'
221
+ ? 1
222
+ : 3600
223
+ );
224
+ return loadAnalytics;
225
+ } catch (e) {
226
+ console.log(e);
227
+ if (e instanceof RefreshToken) {
228
+ return this.checkPostAnalytics(orgId, postId, date, true);
229
+ }
230
+ }
231
+
232
+ return [];
233
+ }
234
+
235
+ async getStatistics(orgId: string, id: string) {
236
+ const getPost = await this.getPostsRecursively(id, true, orgId, true);
237
+ const content = getPost.map((p) => p.content);
238
+ const shortLinksTracking = await this._shortLinkService.getStatistics(
239
+ content
240
+ );
241
+
242
+ return {
243
+ clicks: shortLinksTracking,
244
+ };
245
+ }
246
+
247
+ async mapTypeToPost(
248
+ body: CreatePostDto,
249
+ organization: string,
250
+ replaceDraft: boolean = false
251
+ ): Promise<CreatePostDto> {
252
+ if (!body?.posts?.every((p) => p?.integration?.id)) {
253
+ throw new BadRequestException('All posts must have an integration id');
254
+ }
255
+
256
+ const mappedValues = {
257
+ ...body,
258
+ type: replaceDraft ? 'schedule' : body?.type,
259
+ posts: await Promise.all(
260
+ body?.posts?.map(async (post) => {
261
+ const integration = await this._integrationService.getIntegrationById(
262
+ organization,
263
+ post.integration.id
264
+ );
265
+
266
+ if (!integration) {
267
+ throw new BadRequestException(
268
+ `Integration with id ${post.integration.id} not found`
269
+ );
270
+ }
271
+
272
+ return {
273
+ type: replaceDraft ? 'schedule' : body?.type,
274
+ ...post,
275
+ settings: {
276
+ ...(post.settings || ({} as any)),
277
+ __type: integration.providerIdentifier,
278
+ },
279
+ };
280
+ }) || []
281
+ ),
282
+ };
283
+
284
+ const validationPipe = new ValidationPipe({
285
+ skipMissingProperties: false,
286
+ transform: true,
287
+ transformOptions: {
288
+ enableImplicitConversion: true,
289
+ },
290
+ });
291
+
292
+ return await validationPipe.transform(mappedValues, {
293
+ type: 'body',
294
+ metatype: CreatePostDto,
295
+ });
296
+ }
297
+
298
+ async getPostsRecursively(
299
+ id: string,
300
+ includeIntegration = false,
301
+ orgId?: string,
302
+ isFirst?: boolean
303
+ ): Promise<PostWithConditionals[]> {
304
+ const post = await this._postRepository.getPost(
305
+ id,
306
+ includeIntegration,
307
+ orgId,
308
+ isFirst
309
+ );
310
+
311
+ if (!post) {
312
+ return [];
313
+ }
314
+
315
+ return [
316
+ post!,
317
+ ...(post?.childrenPost?.length
318
+ ? await this.getPostsRecursively(
319
+ post?.childrenPost?.[0]?.id,
320
+ false,
321
+ orgId,
322
+ false
323
+ )
324
+ : []),
325
+ ];
326
+ }
327
+
328
+ async getPosts(orgId: string, query: GetPostsDto) {
329
+ return this._postRepository.getPosts(orgId, query);
330
+ }
331
+
332
+ async getPostsMinified(orgId: string, query: GetPostsDto) {
333
+ return minifyPosts({
334
+ posts: await this._postRepository.getPosts(orgId, query),
335
+ });
336
+ }
337
+
338
+ async getPostsList(orgId: string, query: GetPostsListDto) {
339
+ return minifyPostsList(
340
+ await this._postRepository.getPostsList(orgId, query)
341
+ );
342
+ }
343
+
344
+ async updateMedia(id: string, imagesList: any[], convertToJPEG = false) {
345
+ try {
346
+ let imageUpdateNeeded = false;
347
+ const getImageList = await Promise.all(
348
+ (
349
+ await Promise.all(
350
+ (imagesList || []).map(async (p: any) => {
351
+ if (!p.path && p.id) {
352
+ imageUpdateNeeded = true;
353
+ return this._mediaService.getMediaById(p.id);
354
+ }
355
+
356
+ return p;
357
+ })
358
+ )
359
+ )
360
+ .map((m) => {
361
+ return {
362
+ ...m,
363
+ url:
364
+ m.path.indexOf('http') === -1
365
+ ? process.env.FRONTEND_URL +
366
+ '/' +
367
+ process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
368
+ m.path
369
+ : m.path,
370
+ type: 'image',
371
+ path:
372
+ m.path.indexOf('http') === -1
373
+ ? process.env.UPLOAD_DIRECTORY + m.path
374
+ : m.path,
375
+ };
376
+ })
377
+ .map(async (m) => {
378
+ if (!convertToJPEG) {
379
+ return m;
380
+ }
381
+
382
+ if (hasExtension(m.path, 'png')) {
383
+ imageUpdateNeeded = true;
384
+ const response = await axios.get(m.url, {
385
+ responseType: 'arraybuffer',
386
+ });
387
+
388
+ const imageBuffer = Buffer.from(response.data);
389
+
390
+ // Use sharp to get the metadata of the image
391
+ const buffer = await sharp(imageBuffer)
392
+ .jpeg({ quality: 100 })
393
+ .toBuffer();
394
+
395
+ const { path, originalname } = await this.storage.uploadFile({
396
+ buffer,
397
+ mimetype: 'image/jpeg',
398
+ size: buffer.length,
399
+ path: '',
400
+ fieldname: '',
401
+ destination: '',
402
+ stream: new Readable(),
403
+ filename: '',
404
+ originalname: '',
405
+ encoding: '',
406
+ });
407
+
408
+ return {
409
+ ...m,
410
+ name: originalname,
411
+ url:
412
+ path.indexOf('http') === -1
413
+ ? process.env.FRONTEND_URL +
414
+ '/' +
415
+ process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
416
+ path
417
+ : path,
418
+ type: 'image',
419
+ path:
420
+ path.indexOf('http') === -1
421
+ ? process.env.UPLOAD_DIRECTORY + path
422
+ : path,
423
+ };
424
+ }
425
+
426
+ return m;
427
+ })
428
+ );
429
+
430
+ if (imageUpdateNeeded) {
431
+ await this._postRepository.updateImages(
432
+ id,
433
+ JSON.stringify(getImageList)
434
+ );
435
+ }
436
+
437
+ return getImageList;
438
+ } catch (err: any) {
439
+ return imagesList;
440
+ }
441
+ }
442
+
443
+ async getPostGroupDebugExport(orgId: string, group: string) {
444
+ const loadAll = await this._postRepository.getPostsByGroup(orgId, group);
445
+ const errors = await this._postRepository.getErrorsByPostIds(
446
+ loadAll.map((p) => p.id)
447
+ );
448
+ const posts = this.arrangePostsByGroup(loadAll, undefined);
449
+ const rootPost = posts[0] as any;
450
+
451
+ return {
452
+ type: 'draft' as const,
453
+ shortLink: false,
454
+ date: rootPost.publishDate.toISOString(),
455
+ tags:
456
+ rootPost.tags?.map((t: any) => ({
457
+ value: t.tag.id,
458
+ label: t.tag.name,
459
+ })) || [],
460
+ posts: [
461
+ {
462
+ integration: { id: 'REPLACE_WITH_LOCAL_INTEGRATION_ID' },
463
+ group: rootPost.group,
464
+ settings: JSON.parse(rootPost.settings || '{}'),
465
+ value: posts.map((post) => ({
466
+ content: post.content,
467
+ image: JSON.parse(post.image || '[]'),
468
+ delay: post.delay || 0,
469
+ })),
470
+ },
471
+ ],
472
+ _debug: {
473
+ providerIdentifier: rootPost.integration?.providerIdentifier,
474
+ providerName: rootPost.integration?.name,
475
+ state: rootPost.state,
476
+ error: rootPost.error,
477
+ errors: errors.map((e) => ({
478
+ message: e.message,
479
+ platform: e.platform,
480
+ body: e.body,
481
+ createdAt: e.createdAt,
482
+ })),
483
+ originalGroup: group,
484
+ originalPublishDate: rootPost.publishDate,
485
+ exportedAt: new Date().toISOString(),
486
+ },
487
+ };
488
+ }
489
+
490
+ async getPostsByGroup(orgId: string, group: string) {
491
+ const convertToJPEG = false;
492
+ const loadAll = await this._postRepository.getPostsByGroup(orgId, group);
493
+ const posts = this.arrangePostsByGroup(loadAll, undefined);
494
+
495
+ return {
496
+ group: posts?.[0]?.group,
497
+ posts: await Promise.all(
498
+ (posts || []).map(async (post) => ({
499
+ ...post,
500
+ image: await this.updateMedia(
501
+ post.id,
502
+ JSON.parse(post.image || '[]'),
503
+ convertToJPEG
504
+ ),
505
+ }))
506
+ ),
507
+ integrationPicture: posts[0]?.integration?.picture,
508
+ integration: posts[0].integrationId,
509
+ settings: JSON.parse(posts[0].settings || '{}'),
510
+ };
511
+ }
512
+
513
+ arrangePostsByGroup(all: any, parent?: string): PostWithConditionals[] {
514
+ const findAll = all
515
+ .filter((p: any) =>
516
+ !parent ? !p.parentPostId : p.parentPostId === parent
517
+ )
518
+ .map(({ integration, ...all }: any) => ({
519
+ ...all,
520
+ ...(!parent ? { integration } : {}),
521
+ }));
522
+
523
+ return [
524
+ ...findAll,
525
+ ...(findAll.length
526
+ ? findAll.flatMap((p: any) => this.arrangePostsByGroup(all, p.id))
527
+ : []),
528
+ ];
529
+ }
530
+
531
+ async getPost(orgId: string, id: string, convertToJPEG = false) {
532
+ const posts = await this.getPostsRecursively(id, true, orgId, true);
533
+ const list = {
534
+ group: posts?.[0]?.group,
535
+ posts: await Promise.all(
536
+ (posts || []).map(async (post) => ({
537
+ ...post,
538
+ image: await this.updateMedia(
539
+ post.id,
540
+ JSON.parse(post.image || '[]'),
541
+ convertToJPEG
542
+ ),
543
+ }))
544
+ ),
545
+ integrationPicture: posts[0]?.integration?.picture,
546
+ integration: posts[0].integrationId,
547
+ settings: JSON.parse(posts[0].settings || '{}'),
548
+ };
549
+
550
+ return list;
551
+ }
552
+
553
+ async getOldPosts(orgId: string, date: string) {
554
+ return this._postRepository.getOldPosts(orgId, date);
555
+ }
556
+
557
+ public async updateTags(orgId: string, post: Post[]): Promise<Post[]> {
558
+ const plainText = JSON.stringify(post);
559
+ const extract = Array.from(
560
+ plainText.match(/\(post:[a-zA-Z0-9-_]+\)/g) || []
561
+ );
562
+ if (!extract.length) {
563
+ return post;
564
+ }
565
+
566
+ const ids = (extract || []).map((e) =>
567
+ e.replace('(post:', '').replace(')', '')
568
+ );
569
+ const urls = await this._postRepository.getPostUrls(orgId, ids);
570
+ const newPlainText = ids.reduce((acc, value) => {
571
+ const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || '';
572
+ return acc.replace(
573
+ new RegExp(`\\(post:${value}\\)`, 'g'),
574
+ findUrl.split(',')[0]
575
+ );
576
+ }, plainText);
577
+
578
+ return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]);
579
+ }
580
+
581
+ public async checkInternalPlug(
582
+ integration: Integration,
583
+ orgId: string,
584
+ id: string,
585
+ settings: any
586
+ ) {
587
+ const plugs = Object.entries(settings).filter(([key]) => {
588
+ return key.indexOf('plug-') > -1;
589
+ });
590
+
591
+ if (plugs.length === 0) {
592
+ return [];
593
+ }
594
+
595
+ const parsePlugs = plugs.reduce((all, [key, value]) => {
596
+ const [_, name, identifier] = key.split('--');
597
+ all[name] = all[name] || { name };
598
+ all[name][identifier] = value;
599
+ return all;
600
+ }, {} as any);
601
+
602
+ const list: {
603
+ name: string;
604
+ integrations: { id: string }[];
605
+ delay: string;
606
+ active: boolean;
607
+ }[] = Object.values(parsePlugs);
608
+
609
+ return (list || []).flatMap((trigger) => {
610
+ return (trigger?.integrations || []).flatMap((int) => ({
611
+ type: 'internal-plug',
612
+ post: id,
613
+ originalIntegration: integration.id,
614
+ integration: int.id,
615
+ plugName: trigger.name,
616
+ orgId: orgId,
617
+ delay: +trigger.delay,
618
+ information: trigger,
619
+ }));
620
+ });
621
+ }
622
+
623
+ public async checkPlugs(
624
+ orgId: string,
625
+ providerName: string,
626
+ integrationId: string
627
+ ) {
628
+ const loadAllPlugs = this._integrationManager.getAllPlugs();
629
+ const getPlugs = await this._integrationService.getPlugs(
630
+ orgId,
631
+ integrationId
632
+ );
633
+
634
+ const currentPlug = loadAllPlugs.find((p) => p.identifier === providerName);
635
+
636
+ return getPlugs
637
+ .filter((plug) => {
638
+ return currentPlug?.plugs?.some(
639
+ (p: any) => p.methodName === plug.plugFunction
640
+ );
641
+ })
642
+ .map((plug) => {
643
+ const runPlug = currentPlug?.plugs?.find(
644
+ (p: any) => p.methodName === plug.plugFunction
645
+ )!;
646
+ return {
647
+ type: 'global',
648
+ plugId: plug.id,
649
+ delay: runPlug.runEveryMilliseconds,
650
+ totalRuns: runPlug.totalRuns,
651
+ };
652
+ });
653
+ }
654
+
655
+ async deletePost(orgId: string, group: string) {
656
+ const post = await this._postRepository.deletePost(orgId, group);
657
+
658
+ if (post?.id) {
659
+ try {
660
+ const workflows = this._temporalService.client
661
+ .getRawClient()
662
+ ?.workflow.list({
663
+ query: `postId="${post.id}" AND ExecutionStatus="Running"`,
664
+ });
665
+
666
+ for await (const executionInfo of workflows) {
667
+ try {
668
+ const workflow =
669
+ await this._temporalService.client.getWorkflowHandle(
670
+ executionInfo.workflowId
671
+ );
672
+ if (
673
+ workflow &&
674
+ (await workflow.describe()).status.name !== 'TERMINATED'
675
+ ) {
676
+ await workflow.terminate();
677
+ }
678
+ } catch (err) {}
679
+ }
680
+ } catch (err) {}
681
+ }
682
+
683
+ return { error: true };
684
+ }
685
+
686
+ async countPostsFromDay(orgId: string, date: Date) {
687
+ return this._postRepository.countPostsFromDay(orgId, date);
688
+ }
689
+
690
+ getPostByForWebhookId(id: string) {
691
+ return this._postRepository.getPostByForWebhookId(id);
692
+ }
693
+
694
+ async startWorkflow(
695
+ taskQueue: string,
696
+ postId: string,
697
+ orgId: string,
698
+ state: State
699
+ ) {
700
+ try {
701
+ const workflows = this._temporalService.client
702
+ .getRawClient()
703
+ ?.workflow.list({
704
+ query: `postId="${postId}" AND ExecutionStatus="Running"`,
705
+ });
706
+
707
+ for await (const executionInfo of workflows) {
708
+ try {
709
+ const workflow = await this._temporalService.client.getWorkflowHandle(
710
+ executionInfo.workflowId
711
+ );
712
+ if (
713
+ workflow &&
714
+ (await workflow.describe()).status.name !== 'TERMINATED'
715
+ ) {
716
+ await workflow.terminate();
717
+ }
718
+ } catch (err) {}
719
+ }
720
+ } catch (err) {}
721
+
722
+ if (state === 'DRAFT') {
723
+ return;
724
+ }
725
+
726
+ try {
727
+ await this._temporalService.client
728
+ .getRawClient()
729
+ ?.workflow.start('postWorkflowV105', {
730
+ workflowId: `post_${postId}`,
731
+ taskQueue: 'main',
732
+ workflowIdConflictPolicy: 'TERMINATE_EXISTING',
733
+ args: [
734
+ {
735
+ taskQueue: taskQueue,
736
+ postId: postId,
737
+ organizationId: orgId,
738
+ },
739
+ ],
740
+ typedSearchAttributes: new TypedSearchAttributes([
741
+ {
742
+ key: postIdSearchParam,
743
+ value: postId,
744
+ },
745
+ {
746
+ key: organizationId,
747
+ value: orgId,
748
+ },
749
+ ]),
750
+ });
751
+ } catch (err) {}
752
+ }
753
+
754
+ /**
755
+ * Server-side validation that used to live on the client (`checkValidity` +
756
+ * the manage modal loop). Runs the provider's settings DTO validation, the
757
+ * provider `checkValidity` (media rules) and the empty-content / too-long
758
+ * character checks. Returns one result per post so the frontend can show the
759
+ * same toasts it did before — and so `/posts` can refuse to create invalid
760
+ * posts.
761
+ */
762
+ async validatePosts(
763
+ orgId: string,
764
+ posts: Array<{
765
+ integration: { id: string };
766
+ value: Array<{
767
+ content?: string;
768
+ image?: Array<{ path: string; thumbnail?: string }>;
769
+ }>;
770
+ settings?: any;
771
+ }>
772
+ ) {
773
+ return Promise.all(
774
+ (posts || []).map(async (post) => {
775
+ const integration = await this._integrationService.getIntegrationById(
776
+ orgId,
777
+ post?.integration?.id
778
+ );
779
+
780
+ if (!integration) {
781
+ throw new BadRequestException(
782
+ `Integration with id ${post?.integration?.id} not found`
783
+ );
784
+ }
785
+
786
+ const provider = this._integrationManager.getSocialIntegration(
787
+ integration.providerIdentifier
788
+ );
789
+
790
+ let additionalSettings: any[] = [];
791
+ try {
792
+ additionalSettings = JSON.parse(
793
+ integration.additionalSettings || '[]'
794
+ );
795
+ } catch {
796
+ additionalSettings = [];
797
+ }
798
+
799
+ const settings = post.settings || {};
800
+ const media = (post.value || []).map((p) => p.image || []);
801
+
802
+ // Settings DTO validation — mirrors the client `form.trigger()`.
803
+ let valid = true;
804
+ let settingsError = '';
805
+ if (provider?.dto) {
806
+ const instance = plainToInstance(provider.dto, settings, {
807
+ enableImplicitConversion: false,
808
+ });
809
+ const validationErrors = await validate(instance as object, {
810
+ skipMissingProperties: false,
811
+ });
812
+ settingsError = this.firstValidationError(validationErrors);
813
+ valid = validationErrors.length === 0;
814
+ }
815
+
816
+ // Provider-specific media validation (the old client `checkValidity`).
817
+ let errors: string | true = true;
818
+ try {
819
+ errors = await provider.checkValidity(
820
+ media,
821
+ settings,
822
+ additionalSettings
823
+ );
824
+ } catch (err: any) {
825
+ errors = err?.message || 'Invalid media';
826
+ }
827
+
828
+ const maximumCharacters = provider.maxLength(additionalSettings);
829
+ const isX = integration.providerIdentifier === 'x';
830
+
831
+ const emptyContent = (post.value || []).some((a) => {
832
+ const strip = stripHtmlValidation('normal', a.content || '', true);
833
+ const length = isX ? weightedLength(strip) : strip.length;
834
+ return length === 0 && (a.image || []).length === 0;
835
+ });
836
+
837
+ const tooLong = (post.value || []).some((a) => {
838
+ const strip = stripHtmlValidation('normal', a.content || '', true);
839
+ const weighted = isX ? weightedLength(strip) : strip.length;
840
+ const totalCharacters =
841
+ weighted > strip.length ? weighted : strip.length;
842
+ return totalCharacters > (maximumCharacters || 1000000);
843
+ });
844
+
845
+ return {
846
+ id: integration.id,
847
+ identifier: integration.providerIdentifier,
848
+ name: integration.name,
849
+ valid,
850
+ settingsError,
851
+ errors,
852
+ emptyContent,
853
+ tooLong,
854
+ maximumCharacters,
855
+ };
856
+ })
857
+ );
858
+ }
859
+
860
+ /** Returns the first class-validator message (incl. nested children), or ''. */
861
+ private firstValidationError(errors: any[]): string {
862
+ for (const e of errors || []) {
863
+ if (e?.constraints) {
864
+ return Object.values(e.constraints as Record<string, string>)[0] || '';
865
+ }
866
+ const child = e?.children?.length
867
+ ? this.firstValidationError(e.children)
868
+ : '';
869
+ if (child) {
870
+ return child;
871
+ }
872
+ }
873
+ return '';
874
+ }
875
+
876
+ async createPost(
877
+ orgId: string,
878
+ body: CreatePostDto,
879
+ creationMethod: CreationMethod
880
+ ): Promise<any[]> {
881
+ const postList = [];
882
+ for (const post of body.posts) {
883
+ const provider = this._integrationManager.getSocialIntegration(
884
+ (post.settings as any)?.__type
885
+ );
886
+ const removeLinks = !!provider?.stripLinks?.();
887
+
888
+ const messages = (post.value || []).map((p) => p.content);
889
+ // No point shortlinking links on platforms that strip them out anyway
890
+ const updateContent =
891
+ !body.shortLink || removeLinks
892
+ ? messages
893
+ : await this._shortLinkService.convertTextToShortLinks(
894
+ orgId,
895
+ messages
896
+ );
897
+
898
+ post.value = (post.value || []).map((p, i) => ({
899
+ ...p,
900
+ content: removeLinks ? stripLinks(updateContent[i]) : updateContent[i],
901
+ }));
902
+
903
+ const { posts } = await this._postRepository.createOrUpdatePost(
904
+ body.type,
905
+ orgId,
906
+ body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date,
907
+ post,
908
+ body.tags,
909
+ creationMethod,
910
+ body.inter
911
+ );
912
+
913
+ if (!posts?.length) {
914
+ return [] as any[];
915
+ }
916
+
917
+ if (body.type !== 'update') {
918
+ this.startWorkflow(
919
+ post.settings.__type.split('-')[0].toLowerCase(),
920
+ posts[0].id,
921
+ orgId,
922
+ posts[0].state
923
+ ).catch((err) => {});
924
+ }
925
+
926
+ Sentry.metrics.count('post_created', 1);
927
+ postList.push({
928
+ postId: posts[0].id,
929
+ integration: post.integration.id,
930
+ });
931
+ }
932
+
933
+ return postList;
934
+ }
935
+
936
+ async separatePosts(content: string, len: number) {
937
+ return this._openaiService.separatePosts(content, len);
938
+ }
939
+
940
+ async changeState(id: string, state: State, err?: any, body?: any) {
941
+ return this._postRepository.changeState(id, state, err, body);
942
+ }
943
+
944
+ async changePostStatus(
945
+ orgId: string,
946
+ id: string,
947
+ status: 'draft' | 'schedule'
948
+ ) {
949
+ const getPostById = await this._postRepository.getPostById(id, orgId);
950
+ if (!getPostById) {
951
+ throw new BadRequestException('Post not found');
952
+ }
953
+
954
+ const state: State = status === 'draft' ? 'DRAFT' : 'QUEUE';
955
+ await this._postRepository.changeState(id, state);
956
+
957
+ try {
958
+ await this.startWorkflow(
959
+ getPostById.integration.providerIdentifier.split('-')[0].toLowerCase(),
960
+ getPostById.id,
961
+ orgId,
962
+ state
963
+ );
964
+ } catch (err) {}
965
+
966
+ return { id, state };
967
+ }
968
+
969
+ async changeDate(
970
+ orgId: string,
971
+ id: string,
972
+ date: string,
973
+ action: 'schedule' | 'update' = 'schedule'
974
+ ) {
975
+ const getPostById = await this._postRepository.getPostById(id, orgId);
976
+
977
+ // schedule: Set status to QUEUE and change date (reschedule the post)
978
+ // update: Just change the date without changing the status
979
+ const newDate = await this._postRepository.changeDate(
980
+ orgId,
981
+ id,
982
+ date,
983
+ getPostById.state === 'DRAFT',
984
+ action
985
+ );
986
+
987
+ if (action === 'schedule') {
988
+ try {
989
+ await this.startWorkflow(
990
+ getPostById.integration.providerIdentifier
991
+ .split('-')[0]
992
+ .toLowerCase(),
993
+ getPostById.id,
994
+ orgId,
995
+ getPostById.state === 'DRAFT' ? 'DRAFT' : 'QUEUE'
996
+ );
997
+ } catch (err) {}
998
+ }
999
+
1000
+ return newDate;
1001
+ }
1002
+
1003
+ async generatePostsDraft(orgId: string, body: CreateGeneratedPostsDto) {
1004
+ const getAllIntegrations = (
1005
+ await this._integrationService.getIntegrationsList(orgId)
1006
+ ).filter((f) => !f.disabled && f.providerIdentifier !== 'reddit');
1007
+
1008
+ // const posts = chunk(body.posts, getAllIntegrations.length);
1009
+ const allDates = dayjs()
1010
+ .isoWeek(body.week)
1011
+ .year(body.year)
1012
+ .startOf('isoWeek');
1013
+
1014
+ const dates = [...new Array(7)].map((_, i) => {
1015
+ return allDates.add(i, 'day').format('YYYY-MM-DD');
1016
+ });
1017
+
1018
+ const findTime = (): string => {
1019
+ const totalMinutes = Math.floor(Math.random() * 144) * 10;
1020
+
1021
+ // Convert total minutes to hours and minutes
1022
+ const hours = Math.floor(totalMinutes / 60);
1023
+ const minutes = totalMinutes % 60;
1024
+
1025
+ // Format hours and minutes to always be two digits
1026
+ const formattedHours = hours.toString().padStart(2, '0');
1027
+ const formattedMinutes = minutes.toString().padStart(2, '0');
1028
+ const randomDate =
1029
+ shuffle(dates)[0] + 'T' + `${formattedHours}:${formattedMinutes}:00`;
1030
+
1031
+ if (dayjs(randomDate).isBefore(dayjs())) {
1032
+ return findTime();
1033
+ }
1034
+
1035
+ return randomDate;
1036
+ };
1037
+
1038
+ for (const integration of getAllIntegrations) {
1039
+ for (const toPost of body.posts) {
1040
+ const group = makeId(10);
1041
+ const randomDate = findTime();
1042
+
1043
+ await this.createPost(
1044
+ orgId,
1045
+ {
1046
+ type: 'draft',
1047
+ date: randomDate,
1048
+ order: '',
1049
+ shortLink: false,
1050
+ tags: [],
1051
+ posts: [
1052
+ {
1053
+ group,
1054
+ integration: {
1055
+ id: integration.id,
1056
+ },
1057
+ settings: {
1058
+ __type: integration.providerIdentifier as any,
1059
+ title: '',
1060
+ tags: [],
1061
+ subreddit: [],
1062
+ },
1063
+ value: [
1064
+ ...toPost.list.map((l) => ({
1065
+ id: '',
1066
+ content: l.post,
1067
+ delay: 0,
1068
+ image: [],
1069
+ })),
1070
+ {
1071
+ id: '',
1072
+ delay: 0,
1073
+ content: `Check out the full story here:\n${
1074
+ body.postId || body.url
1075
+ }`,
1076
+ image: [],
1077
+ },
1078
+ ],
1079
+ },
1080
+ ],
1081
+ },
1082
+ 'WEB'
1083
+ );
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ findAllExistingCategories() {
1089
+ return this._postRepository.findAllExistingCategories();
1090
+ }
1091
+
1092
+ findAllExistingTopicsOfCategory(category: string) {
1093
+ return this._postRepository.findAllExistingTopicsOfCategory(category);
1094
+ }
1095
+
1096
+ findPopularPosts(category: string, topic?: string) {
1097
+ return this._postRepository.findPopularPosts(category, topic);
1098
+ }
1099
+
1100
+ async findFreeDateTime(orgId: string, integrationId?: string) {
1101
+ const findTimes = await this._integrationService.findFreeDateTime(
1102
+ orgId,
1103
+ integrationId
1104
+ );
1105
+ return this.findFreeDateTimeRecursive(
1106
+ orgId,
1107
+ findTimes,
1108
+ dayjs.utc().startOf('day')
1109
+ );
1110
+ }
1111
+
1112
+ async createPopularPosts(post: {
1113
+ category: string;
1114
+ topic: string;
1115
+ content: string;
1116
+ hook: string;
1117
+ }) {
1118
+ return this._postRepository.createPopularPosts(post);
1119
+ }
1120
+
1121
+ private async findFreeDateTimeRecursive(
1122
+ orgId: string,
1123
+ times: number[],
1124
+ date: dayjs.Dayjs
1125
+ ): Promise<string> {
1126
+ const list = await this._postRepository.getPostsCountsByDates(
1127
+ orgId,
1128
+ times,
1129
+ date
1130
+ );
1131
+
1132
+ if (!list.length) {
1133
+ return this.findFreeDateTimeRecursive(orgId, times, date.add(1, 'day'));
1134
+ }
1135
+
1136
+ const num = list.reduce<null | number>((prev, curr) => {
1137
+ if (prev === null || prev > curr) {
1138
+ return curr;
1139
+ }
1140
+ return prev;
1141
+ }, null) as number;
1142
+
1143
+ return date.clone().add(num, 'minutes').format('YYYY-MM-DDTHH:mm:00');
1144
+ }
1145
+
1146
+ getComments(postId: string) {
1147
+ return this._postRepository.getComments(postId);
1148
+ }
1149
+
1150
+ getTags(orgId: string) {
1151
+ return this._postRepository.getTags(orgId);
1152
+ }
1153
+
1154
+ createTag(orgId: string, body: CreateTagDto) {
1155
+ return this._postRepository.createTag(orgId, body);
1156
+ }
1157
+
1158
+ editTag(id: string, orgId: string, body: CreateTagDto) {
1159
+ return this._postRepository.editTag(id, orgId, body);
1160
+ }
1161
+
1162
+ deleteTag(id: string, orgId: string) {
1163
+ return this._postRepository.deleteTag(id, orgId);
1164
+ }
1165
+
1166
+ createComment(
1167
+ orgId: string,
1168
+ userId: string,
1169
+ postId: string,
1170
+ comment: string
1171
+ ) {
1172
+ return this._postRepository.createComment(orgId, userId, postId, comment);
1173
+ }
1174
+ }
libraries/nestjs-libraries/src/database/prisma/prisma.service.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ @Injectable()
5
+ export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
6
+ constructor() {
7
+ super({
8
+ log: [
9
+ {
10
+ emit: 'event',
11
+ level: 'query',
12
+ },
13
+ ],
14
+ });
15
+ }
16
+ async onModuleInit() {
17
+ await this.$connect();
18
+ }
19
+
20
+ async onModuleDestroy() {
21
+ await this.$disconnect();
22
+ }
23
+ }
24
+
25
+ @Injectable()
26
+ export class PrismaRepository<T extends keyof PrismaService> {
27
+ public model: Pick<PrismaService, T>;
28
+ constructor(private _prismaService: PrismaService) {
29
+ this.model = this._prismaService;
30
+ }
31
+ }
32
+
33
+ @Injectable()
34
+ export class PrismaTransaction {
35
+ public model: Pick<PrismaService, '$transaction'>;
36
+ constructor(private _prismaService: PrismaService) {
37
+ this.model = this._prismaService;
38
+ }
39
+ }
libraries/nestjs-libraries/src/database/prisma/schema.prisma ADDED
@@ -0,0 +1,970 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ runtime = "nodejs"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ url = env("DATABASE_URL")
9
+ }
10
+
11
+ model Organization {
12
+ id String @id @default(uuid())
13
+ name String
14
+ description String?
15
+ apiKey String?
16
+ paymentId String?
17
+ streakSince DateTime?
18
+ createdAt DateTime @default(now())
19
+ updatedAt DateTime @updatedAt
20
+ allowTrial Boolean @default(false)
21
+ isTrailing Boolean @default(false)
22
+ shortlink ShortLinkPreference @default(ASK)
23
+ autoPost AutoPost[]
24
+ Comments Comments[]
25
+ credits Credits[]
26
+ customers Customer[]
27
+ errors Errors[]
28
+ github GitHub[]
29
+ Integration Integration[]
30
+ media Media[]
31
+ buyerOrganization MessagesGroup[]
32
+ notifications Notifications[]
33
+ plugs Plugs[]
34
+ post Post[] @relation("organization")
35
+ submittedPost Post[] @relation("submittedForOrg")
36
+ sets Sets[]
37
+ signatures Signatures[]
38
+ subscription Subscription?
39
+ tags Tags[]
40
+ thirdParty ThirdParty[]
41
+ usedCodes UsedCodes[]
42
+ users UserOrganization[]
43
+ webhooks Webhooks[]
44
+ oauthApp OAuthApp[]
45
+ oauthAuthorizations OAuthAuthorization[]
46
+
47
+ @@index([apiKey])
48
+ @@index([streakSince])
49
+ @@index([paymentId])
50
+ }
51
+
52
+ model Tags {
53
+ id String @id @default(uuid())
54
+ name String
55
+ color String
56
+ orgId String
57
+ deletedAt DateTime?
58
+ createdAt DateTime @default(now())
59
+ updatedAt DateTime @updatedAt
60
+ organization Organization @relation(fields: [orgId], references: [id])
61
+ posts TagsPosts[]
62
+
63
+ @@index([orgId])
64
+ @@index([deletedAt])
65
+ }
66
+
67
+ model TagsPosts {
68
+ postId String
69
+ tagId String
70
+ createdAt DateTime @default(now())
71
+ updatedAt DateTime @updatedAt
72
+ post Post @relation(fields: [postId], references: [id])
73
+ tag Tags @relation(fields: [tagId], references: [id])
74
+
75
+ @@id([postId, tagId])
76
+ @@unique([postId, tagId])
77
+ }
78
+
79
+ model User {
80
+ id String @id @default(uuid())
81
+ email String
82
+ password String?
83
+ providerName Provider
84
+ name String?
85
+ lastName String?
86
+ isSuperAdmin Boolean @default(false)
87
+ bio String?
88
+ audience Int @default(0)
89
+ pictureId String?
90
+ providerId String?
91
+ timezone Int
92
+ createdAt DateTime @default(now())
93
+ updatedAt DateTime @updatedAt
94
+ lastReadNotifications DateTime @default(now())
95
+ inviteId String?
96
+ activated Boolean @default(true)
97
+ account String?
98
+ connectedAccount Boolean @default(false)
99
+ lastOnline DateTime @default(now())
100
+ ip String?
101
+ agent String?
102
+ comments Comments[]
103
+ items ItemUser[]
104
+ groupBuyer MessagesGroup[] @relation("groupBuyer")
105
+ groupSeller MessagesGroup[] @relation("groupSeller")
106
+ orderBuyer Orders[] @relation("orderBuyer")
107
+ orderSeller Orders[] @relation("orderSeller")
108
+ payoutProblems PayoutProblems[]
109
+ agencies SocialMediaAgency?
110
+ picture Media? @relation(fields: [pictureId], references: [id])
111
+ organizations UserOrganization[]
112
+ sendSuccessEmails Boolean @default(true)
113
+ sendFailureEmails Boolean @default(true)
114
+ sendStreakEmails Boolean @default(true)
115
+ oauthAuthorizations OAuthAuthorization[]
116
+
117
+ @@unique([email, providerName])
118
+ @@index([lastReadNotifications])
119
+ @@index([inviteId])
120
+ @@index([account])
121
+ @@index([lastOnline])
122
+ @@index([pictureId])
123
+ }
124
+
125
+ model UsedCodes {
126
+ id String @id @default(uuid())
127
+ code String
128
+ orgId String
129
+ createdAt DateTime @default(now())
130
+ updatedAt DateTime @updatedAt
131
+ organization Organization @relation(fields: [orgId], references: [id])
132
+
133
+ @@index([code])
134
+ }
135
+
136
+ model UserOrganization {
137
+ id String @id @default(uuid())
138
+ userId String
139
+ organizationId String
140
+ disabled Boolean @default(false)
141
+ role Role @default(USER)
142
+ createdAt DateTime @default(now())
143
+ updatedAt DateTime @updatedAt
144
+ organization Organization @relation(fields: [organizationId], references: [id])
145
+ user User @relation(fields: [userId], references: [id])
146
+
147
+ @@unique([userId, organizationId])
148
+ @@index([disabled])
149
+ }
150
+
151
+ model GitHub {
152
+ id String @id @default(uuid())
153
+ login String?
154
+ name String?
155
+ token String
156
+ jobId String?
157
+ organizationId String
158
+ createdAt DateTime @default(now())
159
+ updatedAt DateTime @updatedAt
160
+ organization Organization @relation(fields: [organizationId], references: [id])
161
+
162
+ @@index([login])
163
+ @@index([organizationId])
164
+ }
165
+
166
+ model Trending {
167
+ id String @id @default(uuid())
168
+ trendingList String
169
+ language String? @unique
170
+ hash String
171
+ date DateTime
172
+ createdAt DateTime @default(now())
173
+ updatedAt DateTime @updatedAt
174
+
175
+ @@index([hash])
176
+ }
177
+
178
+ model TrendingLog {
179
+ id String @id @default(uuid())
180
+ language String?
181
+ date DateTime
182
+ }
183
+
184
+ model ItemUser {
185
+ id String @id @default(uuid())
186
+ userId String
187
+ key String
188
+ user User @relation(fields: [userId], references: [id])
189
+
190
+ @@unique([userId, key])
191
+ @@index([userId])
192
+ @@index([key])
193
+ }
194
+
195
+ model Star {
196
+ id String @id @default(uuid())
197
+ stars Int
198
+ totalStars Int
199
+ forks Int
200
+ totalForks Int
201
+ login String
202
+ date DateTime @default(now()) @db.Date
203
+ createdAt DateTime @default(now())
204
+ updatedAt DateTime @updatedAt
205
+
206
+ @@unique([login, date])
207
+ }
208
+
209
+ model Media {
210
+ id String @id @default(uuid())
211
+ name String
212
+ originalName String?
213
+ path String
214
+ organizationId String
215
+ createdAt DateTime @default(now())
216
+ updatedAt DateTime @updatedAt
217
+ deletedAt DateTime?
218
+ fileSize Int @default(0)
219
+ type String @default("image")
220
+ thumbnail String?
221
+ alt String?
222
+ thumbnailTimestamp Int?
223
+ organization Organization @relation(fields: [organizationId], references: [id])
224
+ agencies SocialMediaAgency[]
225
+ userPicture User[]
226
+ oauthApps OAuthApp[]
227
+
228
+ @@index([name])
229
+ @@index([organizationId])
230
+ @@index([type])
231
+ }
232
+
233
+ model SocialMediaAgency {
234
+ id String @id @default(uuid())
235
+ userId String @unique
236
+ name String
237
+ logoId String?
238
+ website String?
239
+ slug String?
240
+ facebook String?
241
+ instagram String?
242
+ twitter String?
243
+ linkedIn String?
244
+ youtube String?
245
+ tiktok String?
246
+ otherSocialMedia String?
247
+ shortDescription String
248
+ description String
249
+ approved Boolean @default(false)
250
+ createdAt DateTime @default(now())
251
+ updatedAt DateTime @updatedAt
252
+ deletedAt DateTime?
253
+ logo Media? @relation(fields: [logoId], references: [id])
254
+ user User @relation(fields: [userId], references: [id])
255
+ niches SocialMediaAgencyNiche[]
256
+
257
+ @@index([userId])
258
+ @@index([deletedAt])
259
+ @@index([id])
260
+ }
261
+
262
+ model SocialMediaAgencyNiche {
263
+ agencyId String
264
+ niche String
265
+ agency SocialMediaAgency @relation(fields: [agencyId], references: [id])
266
+
267
+ @@id([agencyId, niche])
268
+ }
269
+
270
+ model Credits {
271
+ id String @id @default(uuid())
272
+ organizationId String
273
+ credits Int
274
+ createdAt DateTime @default(now())
275
+ updatedAt DateTime @updatedAt
276
+ type String @default("ai_images")
277
+ organization Organization @relation(fields: [organizationId], references: [id])
278
+
279
+ @@index([organizationId])
280
+ @@index([createdAt])
281
+ }
282
+
283
+ model Subscription {
284
+ id String @id @default(cuid())
285
+ organizationId String @unique
286
+ subscriptionTier SubscriptionTier
287
+ identifier String?
288
+ cancelAt DateTime?
289
+ period Period
290
+ totalChannels Int
291
+ isLifetime Boolean @default(false)
292
+ createdAt DateTime @default(now())
293
+ updatedAt DateTime @updatedAt
294
+ deletedAt DateTime?
295
+ organization Organization @relation(fields: [organizationId], references: [id])
296
+
297
+ @@index([organizationId])
298
+ @@index([deletedAt])
299
+ }
300
+
301
+ model Customer {
302
+ id String @id @default(uuid())
303
+ name String
304
+ orgId String
305
+ createdAt DateTime @default(now())
306
+ updatedAt DateTime @updatedAt
307
+ deletedAt DateTime?
308
+ organization Organization @relation(fields: [orgId], references: [id])
309
+ integrations Integration[]
310
+
311
+ @@unique([orgId, name, deletedAt])
312
+ }
313
+
314
+ model Integration {
315
+ id String @id @default(cuid())
316
+ internalId String
317
+ organizationId String
318
+ name String
319
+ picture String?
320
+ providerIdentifier String
321
+ type String
322
+ token String
323
+ disabled Boolean @default(false)
324
+ tokenExpiration DateTime?
325
+ refreshToken String?
326
+ profile String?
327
+ deletedAt DateTime?
328
+ createdAt DateTime @default(now())
329
+ updatedAt DateTime? @updatedAt
330
+ inBetweenSteps Boolean @default(false)
331
+ refreshNeeded Boolean @default(false)
332
+ postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
333
+ customInstanceDetails String?
334
+ customerId String?
335
+ rootInternalId String?
336
+ additionalSettings String? @default("[]")
337
+ exisingPlugData ExisingPlugData[]
338
+ customer Customer? @relation(fields: [customerId], references: [id])
339
+ organization Organization @relation(fields: [organizationId], references: [id])
340
+ webhooks IntegrationsWebhooks[]
341
+ orderItems OrderItems[]
342
+ plugs Plugs[]
343
+ posts Post[]
344
+
345
+ @@unique([organizationId, internalId])
346
+ @@index([rootInternalId])
347
+ @@index([organizationId])
348
+ @@index([providerIdentifier])
349
+ @@index([updatedAt])
350
+ @@index([createdAt])
351
+ @@index([deletedAt])
352
+ @@index([customerId])
353
+ @@index([inBetweenSteps])
354
+ @@index([refreshNeeded])
355
+ @@index([disabled])
356
+ }
357
+
358
+ model Signatures {
359
+ id String @id @default(uuid())
360
+ organizationId String
361
+ content String
362
+ autoAdd Boolean
363
+ createdAt DateTime @default(now())
364
+ updatedAt DateTime @updatedAt
365
+ deletedAt DateTime?
366
+ organization Organization @relation(fields: [organizationId], references: [id])
367
+
368
+ @@index([createdAt])
369
+ @@index([organizationId])
370
+ @@index([deletedAt])
371
+ }
372
+
373
+ model Comments {
374
+ id String @id @default(uuid())
375
+ content String
376
+ organizationId String
377
+ postId String
378
+ userId String
379
+ createdAt DateTime @default(now())
380
+ updatedAt DateTime @updatedAt
381
+ deletedAt DateTime?
382
+ organization Organization @relation(fields: [organizationId], references: [id])
383
+ post Post @relation(fields: [postId], references: [id])
384
+ user User @relation(fields: [userId], references: [id])
385
+
386
+ @@index([createdAt])
387
+ @@index([organizationId])
388
+ @@index([userId])
389
+ @@index([postId])
390
+ @@index([deletedAt])
391
+ }
392
+
393
+ model Post {
394
+ id String @id @default(cuid())
395
+ state State @default(QUEUE)
396
+ publishDate DateTime
397
+ organizationId String
398
+ integrationId String
399
+ content String
400
+ delay Int @default(0)
401
+ group String
402
+ title String?
403
+ description String?
404
+ parentPostId String?
405
+ releaseId String?
406
+ releaseURL String?
407
+ settings String?
408
+ image String?
409
+ submittedForOrderId String?
410
+ submittedForOrganizationId String?
411
+ approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO)
412
+ creationMethod CreationMethod @default(UNKNOWN)
413
+ lastMessageId String?
414
+ intervalInDays Int?
415
+ error String?
416
+ createdAt DateTime @default(now())
417
+ updatedAt DateTime @updatedAt
418
+ deletedAt DateTime?
419
+ comments Comments[]
420
+ errors Errors[]
421
+ payoutProblems PayoutProblems[]
422
+ integration Integration @relation(fields: [integrationId], references: [id])
423
+ lastMessage Messages? @relation(fields: [lastMessageId], references: [id])
424
+ organization Organization @relation("organization", fields: [organizationId], references: [id])
425
+ parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
426
+ childrenPost Post[] @relation("parentPostId")
427
+ submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id])
428
+ submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id])
429
+ tags TagsPosts[]
430
+
431
+ @@index([group])
432
+ @@index([deletedAt])
433
+ @@index([publishDate])
434
+ @@index([state])
435
+ @@index([organizationId])
436
+ @@index([parentPostId])
437
+ @@index([submittedForOrderId])
438
+ @@index([intervalInDays])
439
+ @@index([approvedSubmitForOrder])
440
+ @@index([creationMethod])
441
+ @@index([lastMessageId])
442
+ @@index([createdAt])
443
+ @@index([updatedAt])
444
+ @@index([releaseURL])
445
+ @@index([integrationId])
446
+ }
447
+
448
+ model Notifications {
449
+ id String @id @default(uuid())
450
+ organizationId String
451
+ content String
452
+ link String?
453
+ createdAt DateTime @default(now())
454
+ updatedAt DateTime @updatedAt
455
+ deletedAt DateTime?
456
+ organization Organization @relation(fields: [organizationId], references: [id])
457
+
458
+ @@index([createdAt])
459
+ @@index([organizationId])
460
+ @@index([deletedAt])
461
+ }
462
+
463
+ model MessagesGroup {
464
+ id String @id @default(uuid())
465
+ buyerOrganizationId String
466
+ buyerId String
467
+ sellerId String
468
+ createdAt DateTime @default(now())
469
+ updatedAt DateTime @updatedAt
470
+ messages Messages[]
471
+ buyer User @relation("groupBuyer", fields: [buyerId], references: [id])
472
+ buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id])
473
+ seller User @relation("groupSeller", fields: [sellerId], references: [id])
474
+ orders Orders[]
475
+
476
+ @@unique([buyerId, sellerId])
477
+ @@index([createdAt])
478
+ @@index([updatedAt])
479
+ @@index([buyerOrganizationId])
480
+ }
481
+
482
+ model PayoutProblems {
483
+ id String @id @default(uuid())
484
+ status String
485
+ orderId String
486
+ userId String
487
+ postId String?
488
+ amount Int
489
+ createdAt DateTime @default(now())
490
+ updatedAt DateTime @updatedAt
491
+ order Orders @relation(fields: [orderId], references: [id])
492
+ post Post? @relation(fields: [postId], references: [id])
493
+ user User @relation(fields: [userId], references: [id])
494
+ }
495
+
496
+ model Orders {
497
+ id String @id @default(uuid())
498
+ buyerId String
499
+ sellerId String
500
+ status OrderStatus
501
+ messageGroupId String
502
+ captureId String?
503
+ createdAt DateTime @default(now())
504
+ updatedAt DateTime @updatedAt
505
+ ordersItems OrderItems[]
506
+ buyer User @relation("orderBuyer", fields: [buyerId], references: [id])
507
+ messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id])
508
+ seller User @relation("orderSeller", fields: [sellerId], references: [id])
509
+ payoutProblems PayoutProblems[]
510
+ posts Post[]
511
+
512
+ @@index([buyerId])
513
+ @@index([sellerId])
514
+ @@index([updatedAt])
515
+ @@index([createdAt])
516
+ @@index([messageGroupId])
517
+ }
518
+
519
+ model OrderItems {
520
+ id String @id @default(uuid())
521
+ orderId String
522
+ integrationId String
523
+ quantity Int
524
+ price Int
525
+ integration Integration @relation(fields: [integrationId], references: [id])
526
+ order Orders @relation(fields: [orderId], references: [id])
527
+
528
+ @@index([orderId])
529
+ @@index([integrationId])
530
+ }
531
+
532
+ model Messages {
533
+ id String @id @default(uuid())
534
+ from From
535
+ content String?
536
+ groupId String
537
+ special String?
538
+ createdAt DateTime @default(now())
539
+ updatedAt DateTime @updatedAt
540
+ deletedAt DateTime?
541
+ group MessagesGroup @relation(fields: [groupId], references: [id])
542
+ posts Post[]
543
+
544
+ @@index([groupId])
545
+ @@index([createdAt])
546
+ @@index([deletedAt])
547
+ }
548
+
549
+ model Plugs {
550
+ id String @id @default(uuid())
551
+ organizationId String
552
+ plugFunction String
553
+ data String
554
+ integrationId String
555
+ activated Boolean @default(true)
556
+ integration Integration @relation(fields: [integrationId], references: [id])
557
+ organization Organization @relation(fields: [organizationId], references: [id])
558
+
559
+ @@unique([plugFunction, integrationId])
560
+ @@index([organizationId])
561
+ }
562
+
563
+ model ExisingPlugData {
564
+ id String @id @default(uuid())
565
+ integrationId String
566
+ methodName String
567
+ value String
568
+ integration Integration @relation(fields: [integrationId], references: [id])
569
+
570
+ @@unique([integrationId, methodName, value])
571
+ }
572
+
573
+ model PopularPosts {
574
+ id String @id @default(uuid())
575
+ category String
576
+ topic String
577
+ content String
578
+ hook String
579
+ createdAt DateTime @default(now())
580
+ updatedAt DateTime @updatedAt
581
+ }
582
+
583
+ model IntegrationsWebhooks {
584
+ integrationId String
585
+ webhookId String
586
+ integration Integration @relation(fields: [integrationId], references: [id])
587
+ webhook Webhooks @relation(fields: [webhookId], references: [id])
588
+
589
+ @@id([integrationId, webhookId])
590
+ @@unique([integrationId, webhookId])
591
+ @@index([integrationId])
592
+ @@index([webhookId])
593
+ }
594
+
595
+ model Webhooks {
596
+ id String @id @default(uuid())
597
+ name String
598
+ organizationId String
599
+ url String
600
+ deletedAt DateTime?
601
+ createdAt DateTime @default(now())
602
+ updatedAt DateTime @updatedAt
603
+ integrations IntegrationsWebhooks[]
604
+ organization Organization @relation(fields: [organizationId], references: [id])
605
+
606
+ @@index([organizationId])
607
+ @@index([deletedAt])
608
+ }
609
+
610
+ model AutoPost {
611
+ id String @id @default(uuid())
612
+ organizationId String
613
+ title String
614
+ content String?
615
+ onSlot Boolean
616
+ syncLast Boolean
617
+ url String
618
+ lastUrl String
619
+ active Boolean
620
+ addPicture Boolean
621
+ generateContent Boolean
622
+ integrations String
623
+ deletedAt DateTime?
624
+ createdAt DateTime @default(now())
625
+ updatedAt DateTime @updatedAt
626
+ organization Organization @relation(fields: [organizationId], references: [id])
627
+
628
+ @@index([deletedAt])
629
+ }
630
+
631
+ model Sets {
632
+ id String @id @default(uuid())
633
+ organizationId String
634
+ name String
635
+ content String
636
+ createdAt DateTime @default(now())
637
+ updatedAt DateTime @updatedAt
638
+ organization Organization @relation(fields: [organizationId], references: [id])
639
+
640
+ @@index([organizationId])
641
+ }
642
+
643
+ model ThirdParty {
644
+ id String @id @default(uuid())
645
+ organizationId String
646
+ identifier String
647
+ name String
648
+ internalId String
649
+ apiKey String
650
+ createdAt DateTime @default(now())
651
+ updatedAt DateTime @updatedAt
652
+ deletedAt DateTime?
653
+ organization Organization @relation(fields: [organizationId], references: [id])
654
+
655
+ @@unique([organizationId, internalId])
656
+ @@index([organizationId])
657
+ @@index([deletedAt])
658
+ }
659
+
660
+ model Errors {
661
+ id String @id @default(uuid())
662
+ message String
663
+ platform String
664
+ organizationId String
665
+ createdAt DateTime @default(now())
666
+ updatedAt DateTime @updatedAt
667
+ postId String
668
+ body String @default("{}")
669
+ organization Organization @relation(fields: [organizationId], references: [id])
670
+ post Post @relation(fields: [postId], references: [id])
671
+
672
+ @@index([organizationId])
673
+ @@index([createdAt])
674
+ }
675
+
676
+ model Mentions {
677
+ name String
678
+ username String
679
+ platform String
680
+ createdAt DateTime @default(now())
681
+ updatedAt DateTime @updatedAt
682
+ image String
683
+
684
+ @@id([name, username, platform, image])
685
+ @@index([createdAt])
686
+ }
687
+
688
+ /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.
689
+ model mastra_ai_spans {
690
+ traceId String
691
+ spanId String
692
+ parentSpanId String?
693
+ name String
694
+ scope Json?
695
+ spanType String
696
+ attributes Json?
697
+ metadata Json?
698
+ links Json?
699
+ input Json?
700
+ output Json?
701
+ error Json?
702
+ startedAt DateTime @db.Timestamp(6)
703
+ endedAt DateTime? @db.Timestamp(6)
704
+ createdAt DateTime @db.Timestamp(6)
705
+ updatedAt DateTime? @db.Timestamp(6)
706
+ isEvent Boolean
707
+ startedAtZ DateTime? @default(now()) @db.Timestamptz(6)
708
+ endedAtZ DateTime? @default(now()) @db.Timestamptz(6)
709
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
710
+ updatedAtZ DateTime? @default(now()) @db.Timestamptz(6)
711
+
712
+ @@index([name], map: "public_mastra_ai_spans_name_idx")
713
+ @@index([parentSpanId, startedAt(sort: Desc)], map: "public_mastra_ai_spans_parentspanid_startedat_idx")
714
+ @@index([spanType, startedAt(sort: Desc)], map: "public_mastra_ai_spans_spantype_startedat_idx")
715
+ @@index([traceId, startedAt(sort: Desc)], map: "public_mastra_ai_spans_traceid_startedat_idx")
716
+ @@ignore
717
+ }
718
+
719
+ /// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.
720
+ model mastra_evals {
721
+ input String
722
+ output String
723
+ result Json
724
+ agent_name String
725
+ metric_name String
726
+ instructions String
727
+ test_info Json?
728
+ global_run_id String
729
+ run_id String
730
+ created_at DateTime @db.Timestamp(6)
731
+ createdAt DateTime? @db.Timestamp(6)
732
+ created_atZ DateTime? @default(now()) @db.Timestamptz(6)
733
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
734
+
735
+ @@index([agent_name, created_at(sort: Desc)], map: "public_mastra_evals_agent_name_created_at_idx")
736
+ @@ignore
737
+ }
738
+
739
+ model mastra_messages {
740
+ id String @id
741
+ thread_id String
742
+ content String
743
+ role String
744
+ type String
745
+ createdAt DateTime @db.Timestamp(6)
746
+ resourceId String?
747
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
748
+
749
+ @@index([thread_id, createdAt(sort: Desc)], map: "public_mastra_messages_thread_id_createdat_idx")
750
+ }
751
+
752
+ model mastra_resources {
753
+ id String @id
754
+ workingMemory String?
755
+ metadata Json?
756
+ createdAt DateTime @db.Timestamp(6)
757
+ updatedAt DateTime @db.Timestamp(6)
758
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
759
+ updatedAtZ DateTime? @default(now()) @db.Timestamptz(6)
760
+ }
761
+
762
+ model mastra_scorers {
763
+ id String @id
764
+ scorerId String
765
+ traceId String?
766
+ runId String
767
+ scorer Json
768
+ preprocessStepResult Json?
769
+ extractStepResult Json?
770
+ analyzeStepResult Json?
771
+ score Float
772
+ reason String?
773
+ metadata Json?
774
+ preprocessPrompt String?
775
+ extractPrompt String?
776
+ generateScorePrompt String?
777
+ generateReasonPrompt String?
778
+ analyzePrompt String?
779
+ reasonPrompt String?
780
+ input Json
781
+ output Json
782
+ additionalContext Json?
783
+ runtimeContext Json?
784
+ entityType String?
785
+ entity Json?
786
+ entityId String?
787
+ source String
788
+ resourceId String?
789
+ threadId String?
790
+ createdAt DateTime @db.Timestamp(6)
791
+ updatedAt DateTime @db.Timestamp(6)
792
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
793
+ updatedAtZ DateTime? @default(now()) @db.Timestamptz(6)
794
+ spanId String?
795
+
796
+ @@index([traceId, spanId, createdAt(sort: Desc)], map: "public_mastra_scores_trace_id_span_id_created_at_idx")
797
+ }
798
+
799
+ model mastra_threads {
800
+ id String @id
801
+ resourceId String
802
+ title String
803
+ metadata String?
804
+ createdAt DateTime @db.Timestamp(6)
805
+ updatedAt DateTime @db.Timestamp(6)
806
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
807
+ updatedAtZ DateTime? @default(now()) @db.Timestamptz(6)
808
+
809
+ @@index([resourceId, createdAt(sort: Desc)], map: "public_mastra_threads_resourceid_createdat_idx")
810
+ }
811
+
812
+ model mastra_traces {
813
+ id String @id
814
+ parentSpanId String?
815
+ name String
816
+ traceId String
817
+ scope String
818
+ kind Int
819
+ attributes Json?
820
+ status Json?
821
+ events Json?
822
+ links Json?
823
+ other String?
824
+ startTime BigInt
825
+ endTime BigInt
826
+ createdAt DateTime @db.Timestamp(6)
827
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
828
+
829
+ @@index([name, startTime(sort: Desc)], map: "public_mastra_traces_name_starttime_idx")
830
+ }
831
+
832
+ model mastra_workflow_snapshot {
833
+ workflow_name String
834
+ run_id String
835
+ resourceId String?
836
+ snapshot String
837
+ createdAt DateTime @db.Timestamp(6)
838
+ updatedAt DateTime @db.Timestamp(6)
839
+ createdAtZ DateTime? @default(now()) @db.Timestamptz(6)
840
+ updatedAtZ DateTime? @default(now()) @db.Timestamptz(6)
841
+
842
+ @@unique([workflow_name, run_id], map: "public_mastra_workflow_snapshot_workflow_name_run_id_key")
843
+ }
844
+
845
+ model OAuthApp {
846
+ id String @id @default(uuid())
847
+ organizationId String
848
+ name String
849
+ description String?
850
+ pictureId String?
851
+ redirectUrl String
852
+ clientId String @unique
853
+ clientSecret String
854
+ createdAt DateTime @default(now())
855
+ updatedAt DateTime @updatedAt
856
+ deletedAt DateTime?
857
+ organization Organization @relation(fields: [organizationId], references: [id])
858
+ picture Media? @relation(fields: [pictureId], references: [id])
859
+ authorizations OAuthAuthorization[]
860
+
861
+ @@unique([organizationId, deletedAt])
862
+ @@index([clientId])
863
+ @@index([organizationId])
864
+ @@index([deletedAt])
865
+ }
866
+
867
+ model OAuthAuthorization {
868
+ id String @id @default(uuid())
869
+ oauthAppId String
870
+ userId String
871
+ organizationId String
872
+ accessToken String?
873
+ authorizationCode String?
874
+ codeExpiresAt DateTime?
875
+ revokedAt DateTime?
876
+ createdAt DateTime @default(now())
877
+ updatedAt DateTime @updatedAt
878
+ oauthApp OAuthApp @relation(fields: [oauthAppId], references: [id])
879
+ user User @relation(fields: [userId], references: [id])
880
+ organization Organization @relation(fields: [organizationId], references: [id])
881
+
882
+ @@unique([oauthAppId, userId, organizationId])
883
+ @@index([accessToken])
884
+ @@index([authorizationCode])
885
+ @@index([oauthAppId])
886
+ @@index([userId])
887
+ @@index([organizationId])
888
+ @@index([revokedAt])
889
+ }
890
+
891
+ enum OrderStatus {
892
+ PENDING
893
+ ACCEPTED
894
+ CANCELED
895
+ COMPLETED
896
+ }
897
+
898
+ enum From {
899
+ BUYER
900
+ SELLER
901
+ }
902
+
903
+ enum State {
904
+ QUEUE
905
+ PUBLISHED
906
+ ERROR
907
+ DRAFT
908
+ }
909
+
910
+ enum SubscriptionTier {
911
+ STANDARD
912
+ PRO
913
+ TEAM
914
+ ULTIMATE
915
+ }
916
+
917
+ enum Period {
918
+ MONTHLY
919
+ YEARLY
920
+ }
921
+
922
+ enum Provider {
923
+ LOCAL
924
+ GITHUB
925
+ GOOGLE
926
+ FARCASTER
927
+ WALLET
928
+ GENERIC
929
+ }
930
+
931
+ enum Role {
932
+ SUPERADMIN
933
+ ADMIN
934
+ USER
935
+ }
936
+
937
+ enum APPROVED_SUBMIT_FOR_ORDER {
938
+ NO
939
+ WAITING_CONFIRMATION
940
+ YES
941
+ }
942
+
943
+ enum CreationMethod {
944
+ UNKNOWN
945
+ WEB
946
+ MCP
947
+ API
948
+ AUTOPOST
949
+ CLI
950
+ }
951
+
952
+ enum ShortLinkPreference {
953
+ ASK
954
+ YES
955
+ NO
956
+ }
957
+
958
+ enum AnnouncementColor {
959
+ INFO
960
+ WARNING
961
+ ERROR
962
+ }
963
+
964
+ model Announcement {
965
+ id String @id @default(uuid())
966
+ title String
967
+ description String
968
+ color AnnouncementColor @default(INFO)
969
+ createdAt DateTime @default(now())
970
+ }
libraries/nestjs-libraries/src/database/prisma/sets/sets.repository.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { SetsDto } from '@gitroom/nestjs-libraries/dtos/sets/sets.dto';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ @Injectable()
7
+ export class SetsRepository {
8
+ constructor(private _sets: PrismaRepository<'sets'>) {}
9
+
10
+ getTotal(orgId: string) {
11
+ return this._sets.model.sets.count({
12
+ where: {
13
+ organizationId: orgId,
14
+ },
15
+ });
16
+ }
17
+
18
+ getSets(orgId: string) {
19
+ return this._sets.model.sets.findMany({
20
+ where: {
21
+ organizationId: orgId,
22
+ },
23
+ orderBy: {
24
+ createdAt: 'desc',
25
+ },
26
+ });
27
+ }
28
+
29
+ deleteSet(orgId: string, id: string) {
30
+ return this._sets.model.sets.delete({
31
+ where: {
32
+ id,
33
+ organizationId: orgId,
34
+ },
35
+ });
36
+ }
37
+
38
+ async createSet(orgId: string, body: SetsDto) {
39
+ const { id } = await this._sets.model.sets.upsert({
40
+ where: {
41
+ id: body.id || uuidv4(),
42
+ organizationId: orgId,
43
+ },
44
+ create: {
45
+ id: body.id || uuidv4(),
46
+ organizationId: orgId,
47
+ name: body.name,
48
+ content: body.content,
49
+ },
50
+ update: {
51
+ name: body.name,
52
+ content: body.content,
53
+ },
54
+ });
55
+
56
+ return { id };
57
+ }
58
+ }
libraries/nestjs-libraries/src/database/prisma/sets/sets.service.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository';
3
+ import { SetsDto } from '@gitroom/nestjs-libraries/dtos/sets/sets.dto';
4
+
5
+ @Injectable()
6
+ export class SetsService {
7
+ constructor(private _setsRepository: SetsRepository) {}
8
+
9
+ getTotal(orgId: string) {
10
+ return this._setsRepository.getTotal(orgId);
11
+ }
12
+
13
+ getSets(orgId: string) {
14
+ return this._setsRepository.getSets(orgId);
15
+ }
16
+
17
+ createSet(orgId: string, body: SetsDto) {
18
+ return this._setsRepository.createSet(orgId, body);
19
+ }
20
+
21
+ deleteSet(orgId: string, id: string) {
22
+ return this._setsRepository.deleteSet(orgId, id);
23
+ }
24
+ }
libraries/nestjs-libraries/src/database/prisma/signatures/signature.repository.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { SignatureDto } from '@gitroom/nestjs-libraries/dtos/signature/signature.dto';
5
+
6
+ @Injectable()
7
+ export class SignatureRepository {
8
+ constructor(private _signatures: PrismaRepository<'signatures'>) {}
9
+
10
+ getSignaturesByOrgId(orgId: string) {
11
+ return this._signatures.model.signatures.findMany({
12
+ where: { organizationId: orgId, deletedAt: null },
13
+ });
14
+ }
15
+
16
+ getDefaultSignature(orgId: string) {
17
+ return this._signatures.model.signatures.findFirst({
18
+ where: { organizationId: orgId, autoAdd: true, deletedAt: null },
19
+ });
20
+ }
21
+
22
+ async createOrUpdateSignature(
23
+ orgId: string,
24
+ signature: SignatureDto,
25
+ id?: string
26
+ ) {
27
+ const values = {
28
+ organizationId: orgId,
29
+ content: signature.content,
30
+ autoAdd: signature.autoAdd,
31
+ };
32
+
33
+ const { id: updatedId } = await this._signatures.model.signatures.upsert({
34
+ where: { id: id || uuidv4(), organizationId: orgId },
35
+ update: values,
36
+ create: values,
37
+ });
38
+
39
+ if (values.autoAdd) {
40
+ await this._signatures.model.signatures.updateMany({
41
+ where: { organizationId: orgId, id: { not: updatedId } },
42
+ data: { autoAdd: false },
43
+ });
44
+ }
45
+
46
+ return { id: updatedId };
47
+ }
48
+
49
+ deleteSignature(orgId: string, id: string) {
50
+ return this._signatures.model.signatures.update({
51
+ where: { id, organizationId: orgId },
52
+ data: { deletedAt: new Date() },
53
+ });
54
+ }
55
+ }
libraries/nestjs-libraries/src/database/prisma/signatures/signature.service.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { SignatureRepository } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.repository';
3
+ import { SignatureDto } from '@gitroom/nestjs-libraries/dtos/signature/signature.dto';
4
+
5
+ @Injectable()
6
+ export class SignatureService {
7
+ constructor(private _signatureRepository: SignatureRepository) {}
8
+
9
+ getSignaturesByOrgId(orgId: string) {
10
+ return this._signatureRepository.getSignaturesByOrgId(orgId);
11
+ }
12
+
13
+ getDefaultSignature(orgId: string) {
14
+ return this._signatureRepository.getDefaultSignature(orgId);
15
+ }
16
+
17
+ createOrUpdateSignature(orgId: string, signature: SignatureDto, id?: string) {
18
+ return this._signatureRepository.createOrUpdateSignature(
19
+ orgId,
20
+ signature,
21
+ id
22
+ );
23
+ }
24
+
25
+ deleteSignature(orgId: string, id: string) {
26
+ return this._signatureRepository.deleteSignature(orgId, id);
27
+ }
28
+ }
libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface PricingInnerInterface {
2
+ current: string;
3
+ month_price: number;
4
+ year_price: number;
5
+ channel?: number;
6
+ posts_per_month: number;
7
+ team_members: boolean;
8
+ community_features: boolean;
9
+ featured_by_gitroom: boolean;
10
+ ai: boolean;
11
+ import_from_channels: boolean;
12
+ image_generator?: boolean;
13
+ image_generation_count: number;
14
+ generate_videos: number;
15
+ public_api: boolean;
16
+ webhooks: number;
17
+ autoPost: boolean;
18
+ }
19
+ export interface PricingInterface {
20
+ [key: string]: PricingInnerInterface;
21
+ }
22
+ export const pricing: PricingInterface = {
23
+ FREE: {
24
+ current: 'FREE',
25
+ month_price: 0,
26
+ year_price: 0,
27
+ channel: 0,
28
+ image_generation_count: 0,
29
+ posts_per_month: 0,
30
+ team_members: false,
31
+ community_features: false,
32
+ featured_by_gitroom: false,
33
+ ai: false,
34
+ import_from_channels: false,
35
+ image_generator: false,
36
+ public_api: false,
37
+ webhooks: 0,
38
+ autoPost: false,
39
+ generate_videos: 0,
40
+ },
41
+ STANDARD: {
42
+ current: 'STANDARD',
43
+ month_price: 29,
44
+ year_price: 278,
45
+ channel: 5,
46
+ posts_per_month: 400,
47
+ image_generation_count: 20,
48
+ team_members: false,
49
+ ai: true,
50
+ community_features: false,
51
+ featured_by_gitroom: false,
52
+ import_from_channels: true,
53
+ image_generator: false,
54
+ public_api: true,
55
+ webhooks: 2,
56
+ autoPost: false,
57
+ generate_videos: 3,
58
+ },
59
+ TEAM: {
60
+ current: 'TEAM',
61
+ month_price: 39,
62
+ year_price: 374,
63
+ channel: 10,
64
+ posts_per_month: 1000000,
65
+ image_generation_count: 100,
66
+ community_features: true,
67
+ team_members: true,
68
+ featured_by_gitroom: true,
69
+ ai: true,
70
+ import_from_channels: true,
71
+ image_generator: true,
72
+ public_api: true,
73
+ webhooks: 10,
74
+ autoPost: true,
75
+ generate_videos: 10,
76
+ },
77
+ PRO: {
78
+ current: 'PRO',
79
+ month_price: 49,
80
+ year_price: 470,
81
+ channel: 30,
82
+ posts_per_month: 1000000,
83
+ image_generation_count: 300,
84
+ community_features: true,
85
+ team_members: true,
86
+ featured_by_gitroom: true,
87
+ ai: true,
88
+ import_from_channels: true,
89
+ image_generator: true,
90
+ public_api: true,
91
+ webhooks: 30,
92
+ autoPost: true,
93
+ generate_videos: 30,
94
+ },
95
+ ULTIMATE: {
96
+ current: 'ULTIMATE',
97
+ month_price: 99,
98
+ year_price: 950,
99
+ channel: 100,
100
+ posts_per_month: 1000000,
101
+ image_generation_count: 500,
102
+ community_features: true,
103
+ team_members: true,
104
+ featured_by_gitroom: true,
105
+ ai: true,
106
+ import_from_channels: true,
107
+ image_generator: true,
108
+ public_api: true,
109
+ webhooks: 10000,
110
+ autoPost: true,
111
+ generate_videos: 60,
112
+ },
113
+ };
libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import {
3
+ PrismaRepository,
4
+ PrismaTransaction,
5
+ } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
6
+ import dayjs from 'dayjs';
7
+ import { Organization } from '@prisma/client';
8
+
9
+ @Injectable()
10
+ export class SubscriptionRepository {
11
+ constructor(
12
+ private readonly _subscription: PrismaRepository<'subscription'>,
13
+ private readonly _organization: PrismaRepository<'organization'>,
14
+ private readonly _user: PrismaRepository<'user'>,
15
+ private readonly _credits: PrismaRepository<'credits'>,
16
+ private _usedCodes: PrismaRepository<'usedCodes'>
17
+ ) {}
18
+
19
+ getUserAccount(userId: string) {
20
+ return this._user.model.user.findFirst({
21
+ where: {
22
+ id: userId,
23
+ },
24
+ select: {
25
+ account: true,
26
+ connectedAccount: true,
27
+ },
28
+ });
29
+ }
30
+
31
+ getCode(code: string) {
32
+ return this._usedCodes.model.usedCodes.findFirst({
33
+ where: {
34
+ code,
35
+ },
36
+ });
37
+ }
38
+
39
+ updateAccount(userId: string, account: string) {
40
+ return this._user.model.user.update({
41
+ where: {
42
+ id: userId,
43
+ },
44
+ data: {
45
+ account,
46
+ },
47
+ });
48
+ }
49
+
50
+ getSubscriptionByOrganizationId(organizationId: string) {
51
+ return this._subscription.model.subscription.findFirst({
52
+ where: {
53
+ organizationId,
54
+ deletedAt: null,
55
+ },
56
+ });
57
+ }
58
+
59
+ updateConnectedStatus(account: string, accountCharges: boolean) {
60
+ return this._user.model.user.updateMany({
61
+ where: {
62
+ account,
63
+ },
64
+ data: {
65
+ connectedAccount: accountCharges,
66
+ },
67
+ });
68
+ }
69
+
70
+ getCustomerIdByOrgId(organizationId: string) {
71
+ return this._organization.model.organization.findFirst({
72
+ where: {
73
+ id: organizationId,
74
+ },
75
+ select: {
76
+ paymentId: true,
77
+ },
78
+ });
79
+ }
80
+
81
+ checkSubscription(organizationId: string, subscriptionId: string) {
82
+ return this._subscription.model.subscription.findFirst({
83
+ where: {
84
+ organizationId,
85
+ identifier: subscriptionId,
86
+ deletedAt: null,
87
+ },
88
+ });
89
+ }
90
+
91
+ deleteSubscriptionByCustomerId(customerId: string) {
92
+ return this._subscription.model.subscription.deleteMany({
93
+ where: {
94
+ organization: {
95
+ paymentId: customerId,
96
+ },
97
+ },
98
+ });
99
+ }
100
+
101
+ updateCustomerId(organizationId: string, customerId: string) {
102
+ return this._organization.model.organization.update({
103
+ where: {
104
+ id: organizationId,
105
+ },
106
+ data: {
107
+ paymentId: customerId,
108
+ },
109
+ });
110
+ }
111
+
112
+ async getSubscriptionByOrgId(orgId: string) {
113
+ return this._subscription.model.subscription.findFirst({
114
+ where: {
115
+ organizationId: orgId,
116
+ },
117
+ });
118
+ }
119
+
120
+ async getSubscriptionByCustomerId(customerId: string) {
121
+ return this._subscription.model.subscription.findFirst({
122
+ where: {
123
+ organization: {
124
+ paymentId: customerId,
125
+ },
126
+ },
127
+ });
128
+ }
129
+
130
+ async getOrganizationByCustomerId(customerId: string) {
131
+ return this._organization.model.organization.findFirst({
132
+ where: {
133
+ paymentId: customerId,
134
+ },
135
+ });
136
+ }
137
+
138
+ async createOrUpdateSubscription(
139
+ isTrailing: boolean,
140
+ identifier: string,
141
+ customerId: string,
142
+ totalChannels: number,
143
+ billing: 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE',
144
+ period: 'MONTHLY' | 'YEARLY',
145
+ cancelAt: number | null,
146
+ code?: string,
147
+ org?: { id: string }
148
+ ) {
149
+ const findOrg =
150
+ org || (await this.getOrganizationByCustomerId(customerId))!;
151
+
152
+ if (!findOrg) {
153
+ return;
154
+ }
155
+
156
+ await this._subscription.model.subscription.upsert({
157
+ where: {
158
+ organizationId: findOrg.id,
159
+ ...(!code
160
+ ? {
161
+ organization: {
162
+ paymentId: customerId,
163
+ },
164
+ }
165
+ : {}),
166
+ },
167
+ update: {
168
+ subscriptionTier: billing,
169
+ totalChannels,
170
+ period,
171
+ identifier,
172
+ isLifetime: !!code,
173
+ cancelAt: cancelAt ? new Date(cancelAt * 1000) : null,
174
+ deletedAt: null,
175
+ },
176
+ create: {
177
+ organizationId: findOrg.id,
178
+ subscriptionTier: billing,
179
+ isLifetime: !!code,
180
+ totalChannels,
181
+ period,
182
+ cancelAt: cancelAt ? new Date(cancelAt * 1000) : null,
183
+ identifier,
184
+ deletedAt: null,
185
+ },
186
+ });
187
+
188
+ await this._organization.model.organization.update({
189
+ where: {
190
+ id: findOrg.id,
191
+ },
192
+ data: {
193
+ isTrailing,
194
+ allowTrial: false,
195
+ },
196
+ });
197
+
198
+ if (code) {
199
+ await this._usedCodes.model.usedCodes.create({
200
+ data: {
201
+ code,
202
+ orgId: findOrg.id,
203
+ },
204
+ });
205
+ }
206
+ }
207
+
208
+ getSubscriptionByIdentifier(identifier: string) {
209
+ return this._subscription.model.subscription.findFirst({
210
+ where: {
211
+ identifier,
212
+ deletedAt: null,
213
+ },
214
+ include: {
215
+ organization: true,
216
+ },
217
+ });
218
+ }
219
+
220
+ getSubscription(organizationId: string) {
221
+ return this._subscription.model.subscription.findFirst({
222
+ where: {
223
+ organizationId,
224
+ deletedAt: null,
225
+ },
226
+ });
227
+ }
228
+
229
+ async getCreditsFrom(
230
+ organizationId: string,
231
+ from: dayjs.Dayjs,
232
+ type = 'ai_images'
233
+ ) {
234
+ const load = await this._credits.model.credits.groupBy({
235
+ by: ['organizationId'],
236
+ where: {
237
+ organizationId,
238
+ type,
239
+ createdAt: {
240
+ gte: from.toDate(),
241
+ },
242
+ },
243
+ _sum: {
244
+ credits: true,
245
+ },
246
+ });
247
+
248
+ return load?.[0]?._sum?.credits || 0;
249
+ }
250
+
251
+ async useCredit<T>(
252
+ org: Organization,
253
+ type = 'ai_images',
254
+ func: () => Promise<T>
255
+ ) {
256
+ const data = await this._credits.model.credits.create({
257
+ data: {
258
+ organizationId: org.id,
259
+ credits: 1,
260
+ type,
261
+ },
262
+ });
263
+
264
+ try {
265
+ return await func();
266
+ } catch (err) {
267
+ await this._credits.model.credits.delete({
268
+ where: {
269
+ id: data.id,
270
+ },
271
+ });
272
+ throw err;
273
+ }
274
+ }
275
+
276
+ setCustomerId(orgId: string, customerId: string) {
277
+ return this._organization.model.organization.update({
278
+ where: {
279
+ id: orgId,
280
+ },
281
+ data: {
282
+ paymentId: customerId,
283
+ },
284
+ });
285
+ }
286
+ }
libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
3
+ import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository';
4
+ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
5
+ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
6
+ import { Organization } from '@prisma/client';
7
+ import dayjs from 'dayjs';
8
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
9
+
10
+ @Injectable()
11
+ export class SubscriptionService {
12
+ constructor(
13
+ private readonly _subscriptionRepository: SubscriptionRepository,
14
+ private readonly _integrationService: IntegrationService,
15
+ private readonly _organizationService: OrganizationService
16
+ ) {}
17
+
18
+ getSubscriptionByOrganizationId(organizationId: string) {
19
+ return this._subscriptionRepository.getSubscriptionByOrganizationId(
20
+ organizationId
21
+ );
22
+ }
23
+
24
+ useCredit<T>(
25
+ organization: Organization,
26
+ type = 'ai_images',
27
+ func: () => Promise<T>
28
+ ): Promise<T> {
29
+ return this._subscriptionRepository.useCredit(organization, type, func);
30
+ }
31
+
32
+ getCode(code: string) {
33
+ return this._subscriptionRepository.getCode(code);
34
+ }
35
+
36
+ async deleteSubscription(customerId: string) {
37
+ await this.modifySubscription(
38
+ customerId,
39
+ pricing.FREE.channel || 0,
40
+ 'FREE'
41
+ );
42
+ return this._subscriptionRepository.deleteSubscriptionByCustomerId(
43
+ customerId
44
+ );
45
+ }
46
+
47
+ updateCustomerId(organizationId: string, customerId: string) {
48
+ return this._subscriptionRepository.updateCustomerId(
49
+ organizationId,
50
+ customerId
51
+ );
52
+ }
53
+
54
+ async checkSubscription(organizationId: string, subscriptionId: string) {
55
+ return await this._subscriptionRepository.checkSubscription(
56
+ organizationId,
57
+ subscriptionId
58
+ );
59
+ }
60
+
61
+ async modifySubscriptionByOrg(
62
+ organizationId: string,
63
+ totalChannels: number,
64
+ billing: 'FREE' | 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE'
65
+ ) {
66
+ if (!organizationId) {
67
+ return false;
68
+ }
69
+
70
+ const getCurrentSubscription =
71
+ (await this._subscriptionRepository.getSubscriptionByOrgId(
72
+ organizationId
73
+ ))!;
74
+
75
+ const from = pricing[getCurrentSubscription?.subscriptionTier || 'FREE'];
76
+ const to = pricing[billing];
77
+
78
+ const currentTotalChannels = (
79
+ await this._integrationService.getIntegrationsList(organizationId)
80
+ ).filter((f) => !f.disabled);
81
+
82
+ if (currentTotalChannels.length > totalChannels) {
83
+ await this._integrationService.disableIntegrations(
84
+ organizationId,
85
+ currentTotalChannels.length - totalChannels
86
+ );
87
+ }
88
+
89
+ if (from.team_members && !to.team_members) {
90
+ await this._organizationService.disableOrEnableNonSuperAdminUsers(
91
+ organizationId,
92
+ true
93
+ );
94
+ }
95
+
96
+ if (!from.team_members && to.team_members) {
97
+ await this._organizationService.disableOrEnableNonSuperAdminUsers(
98
+ organizationId,
99
+ false
100
+ );
101
+ }
102
+
103
+ if (billing === 'FREE') {
104
+ await this._integrationService.changeActiveCron(organizationId);
105
+ }
106
+
107
+ return true;
108
+ }
109
+
110
+ async modifySubscription(
111
+ customerId: string,
112
+ totalChannels: number,
113
+ billing: 'FREE' | 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE'
114
+ ) {
115
+ if (!customerId) {
116
+ return false;
117
+ }
118
+
119
+ const getOrgByCustomerId =
120
+ await this._subscriptionRepository.getOrganizationByCustomerId(
121
+ customerId
122
+ );
123
+
124
+ const getCurrentSubscription =
125
+ (await this._subscriptionRepository.getSubscriptionByCustomerId(
126
+ customerId
127
+ ))!;
128
+
129
+ if (
130
+ !getOrgByCustomerId ||
131
+ (getCurrentSubscription && getCurrentSubscription?.isLifetime)
132
+ ) {
133
+ return false;
134
+ }
135
+
136
+ const from = pricing[getCurrentSubscription?.subscriptionTier || 'FREE'];
137
+ const to = pricing[billing];
138
+
139
+ const currentTotalChannels = (
140
+ await this._integrationService.getIntegrationsList(
141
+ getOrgByCustomerId?.id!
142
+ )
143
+ ).filter((f) => !f.disabled);
144
+
145
+ if (currentTotalChannels.length > totalChannels) {
146
+ await this._integrationService.disableIntegrations(
147
+ getOrgByCustomerId?.id!,
148
+ currentTotalChannels.length - totalChannels
149
+ );
150
+ }
151
+
152
+ if (from.team_members && !to.team_members) {
153
+ await this._organizationService.disableOrEnableNonSuperAdminUsers(
154
+ getOrgByCustomerId?.id!,
155
+ true
156
+ );
157
+ }
158
+
159
+ if (!from.team_members && to.team_members) {
160
+ await this._organizationService.disableOrEnableNonSuperAdminUsers(
161
+ getOrgByCustomerId?.id!,
162
+ false
163
+ );
164
+ }
165
+
166
+ if (billing === 'FREE') {
167
+ await this._integrationService.changeActiveCron(getOrgByCustomerId?.id!);
168
+ }
169
+
170
+ return true;
171
+ }
172
+
173
+ async createOrUpdateSubscription(
174
+ isTrailing: boolean,
175
+ identifier: string,
176
+ customerId: string,
177
+ totalChannels: number,
178
+ billing: 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE',
179
+ period: 'MONTHLY' | 'YEARLY',
180
+ cancelAt: number | null,
181
+ code?: string,
182
+ org?: string
183
+ ) {
184
+ if (!code) {
185
+ try {
186
+ const load = await this.modifySubscription(
187
+ customerId,
188
+ totalChannels,
189
+ billing
190
+ );
191
+ if (!load) {
192
+ return {};
193
+ }
194
+ } catch (e) {
195
+ return {};
196
+ }
197
+ }
198
+ return this._subscriptionRepository.createOrUpdateSubscription(
199
+ isTrailing,
200
+ identifier,
201
+ customerId,
202
+ totalChannels,
203
+ billing,
204
+ period,
205
+ cancelAt,
206
+ code,
207
+ org ? { id: org } : undefined
208
+ );
209
+ }
210
+
211
+ getSubscriptionByIdentifier(identifier: string) {
212
+ return this._subscriptionRepository.getSubscriptionByIdentifier(identifier);
213
+ }
214
+
215
+ async getSubscription(organizationId: string) {
216
+ return this._subscriptionRepository.getSubscription(organizationId);
217
+ }
218
+
219
+ async checkCredits(organization: Organization, checkType = 'ai_images') {
220
+ // @ts-ignore
221
+ const type = organization?.subscription?.subscriptionTier || 'FREE';
222
+
223
+ if (type === 'FREE') {
224
+ return { credits: 0 };
225
+ }
226
+
227
+ // @ts-ignore
228
+ let date = dayjs(organization.subscription.createdAt);
229
+ while (date.isBefore(dayjs())) {
230
+ date = date.add(1, 'month');
231
+ }
232
+
233
+ const checkFromMonth = date.subtract(1, 'month');
234
+ const imageGenerationCount =
235
+ checkType === 'ai_images'
236
+ ? pricing[type].image_generation_count
237
+ : pricing[type].generate_videos;
238
+
239
+ const totalUse = await this._subscriptionRepository.getCreditsFrom(
240
+ organization.id,
241
+ checkFromMonth,
242
+ checkType
243
+ );
244
+
245
+ return {
246
+ credits: imageGenerationCount - totalUse,
247
+ };
248
+ }
249
+
250
+ async addSubscription(orgId: string, userId: string, subscription: any) {
251
+ await this._subscriptionRepository.setCustomerId(orgId, userId);
252
+ return this.createOrUpdateSubscription(
253
+ false,
254
+ makeId(5),
255
+ userId,
256
+ pricing[subscription].channel!,
257
+ subscription,
258
+ 'MONTHLY',
259
+ null,
260
+ undefined,
261
+ orgId
262
+ );
263
+ }
264
+ }
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.repository.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { AuthService } from '@gitroom/helpers/auth/auth.service';
4
+
5
+ @Injectable()
6
+ export class ThirdPartyRepository {
7
+ constructor(private _thirdParty: PrismaRepository<'thirdParty'>) {}
8
+
9
+ getAllThirdPartiesByOrganization(org: string) {
10
+ return this._thirdParty.model.thirdParty.findMany({
11
+ where: { organizationId: org, deletedAt: null },
12
+ select: {
13
+ id: true,
14
+ name: true,
15
+ identifier: true,
16
+ },
17
+ });
18
+ }
19
+
20
+ deleteIntegration(org: string, id: string) {
21
+ return this._thirdParty.model.thirdParty.update({
22
+ where: { id, organizationId: org },
23
+ data: { deletedAt: new Date() },
24
+ });
25
+ }
26
+
27
+ getIntegrationById(org: string, id: string) {
28
+ return this._thirdParty.model.thirdParty.findFirst({
29
+ where: { id, organizationId: org, deletedAt: null },
30
+ });
31
+ }
32
+
33
+ saveIntegration(
34
+ org: string,
35
+ identifier: string,
36
+ apiKey: string,
37
+ data: { name: string; username: string; id: string }
38
+ ) {
39
+ return this._thirdParty.model.thirdParty.upsert({
40
+ where: {
41
+ organizationId_internalId: {
42
+ internalId: data.id,
43
+ organizationId: org,
44
+ },
45
+ },
46
+ create: {
47
+ organizationId: org,
48
+ name: data.name,
49
+ internalId: data.id,
50
+ identifier,
51
+ apiKey: AuthService.fixedEncryption(apiKey),
52
+ deletedAt: null,
53
+ },
54
+ update: {
55
+ organizationId: org,
56
+ name: data.name,
57
+ internalId: data.id,
58
+ identifier,
59
+ apiKey: AuthService.fixedEncryption(apiKey),
60
+ deletedAt: null,
61
+ },
62
+ });
63
+ }
64
+ }
libraries/nestjs-libraries/src/database/prisma/third-party/third-party.service.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ThirdPartyRepository } from '@gitroom/nestjs-libraries/database/prisma/third-party/third-party.repository';
3
+
4
+ @Injectable()
5
+ export class ThirdPartyService {
6
+ constructor(private _thirdPartyRepository: ThirdPartyRepository) {}
7
+
8
+ getAllThirdPartiesByOrganization(org: string) {
9
+ return this._thirdPartyRepository.getAllThirdPartiesByOrganization(org);
10
+ }
11
+
12
+ deleteIntegration(org: string, id: string) {
13
+ return this._thirdPartyRepository.deleteIntegration(org, id);
14
+ }
15
+
16
+ getIntegrationById(org: string, id: string) {
17
+ return this._thirdPartyRepository.getIntegrationById(org, id);
18
+ }
19
+
20
+ saveIntegration(
21
+ org: string,
22
+ identifier: string,
23
+ apiKey: string,
24
+ data: { name: string; username: string; id: string }
25
+ ) {
26
+ return this._thirdPartyRepository.saveIntegration(org, identifier, apiKey, data);
27
+ }
28
+ }
libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { Provider } from '@prisma/client';
4
+ import { AuthService } from '@gitroom/helpers/auth/auth.service';
5
+ import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
6
+ import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
7
+
8
+ @Injectable()
9
+ export class UsersRepository {
10
+ constructor(private _user: PrismaRepository<'user'>) {}
11
+
12
+ getImpersonateUser(name: string) {
13
+ return this._user.model.user.findMany({
14
+ where: {
15
+ OR: [
16
+ {
17
+ name: {
18
+ contains: name,
19
+ },
20
+ },
21
+ {
22
+ email: {
23
+ contains: name,
24
+ },
25
+ },
26
+ {
27
+ id: {
28
+ contains: name,
29
+ },
30
+ },
31
+ ],
32
+ },
33
+ select: {
34
+ id: true,
35
+ name: true,
36
+ email: true,
37
+ },
38
+ take: 10,
39
+ });
40
+ }
41
+
42
+ getUserById(id: string) {
43
+ return this._user.model.user.findFirst({
44
+ where: {
45
+ id,
46
+ },
47
+ });
48
+ }
49
+
50
+ getUserByEmail(email: string) {
51
+ return this._user.model.user.findFirst({
52
+ where: {
53
+ email,
54
+ providerName: Provider.LOCAL,
55
+ },
56
+ include: {
57
+ picture: {
58
+ select: {
59
+ id: true,
60
+ path: true,
61
+ },
62
+ },
63
+ },
64
+ });
65
+ }
66
+
67
+ activateUser(id: string) {
68
+ return this._user.model.user.update({
69
+ where: {
70
+ id,
71
+ },
72
+ data: {
73
+ activated: true,
74
+ },
75
+ });
76
+ }
77
+
78
+ getUserByProvider(providerId: string, provider: Provider) {
79
+ return this._user.model.user.findFirst({
80
+ where: {
81
+ providerId,
82
+ providerName: provider,
83
+ },
84
+ });
85
+ }
86
+
87
+ updatePassword(id: string, password: string) {
88
+ return this._user.model.user.update({
89
+ where: {
90
+ id,
91
+ providerName: Provider.LOCAL,
92
+ },
93
+ data: {
94
+ password: AuthService.hashPassword(password),
95
+ },
96
+ });
97
+ }
98
+
99
+ changeAudienceSize(userId: string, audience: number) {
100
+ return this._user.model.user.update({
101
+ where: {
102
+ id: userId,
103
+ },
104
+ data: {
105
+ audience,
106
+ },
107
+ });
108
+ }
109
+
110
+ async getPersonal(userId: string) {
111
+ const user = await this._user.model.user.findUnique({
112
+ where: {
113
+ id: userId,
114
+ },
115
+ select: {
116
+ id: true,
117
+ name: true,
118
+ bio: true,
119
+ picture: {
120
+ select: {
121
+ id: true,
122
+ path: true,
123
+ },
124
+ },
125
+ },
126
+ });
127
+
128
+ return user;
129
+ }
130
+
131
+ async changePersonal(userId: string, body: UserDetailDto) {
132
+ await this._user.model.user.update({
133
+ where: {
134
+ id: userId,
135
+ },
136
+ data: {
137
+ name: body.fullname,
138
+ bio: body.bio,
139
+ picture: body.picture
140
+ ? {
141
+ connect: {
142
+ id: body.picture.id,
143
+ },
144
+ }
145
+ : {
146
+ disconnect: true,
147
+ },
148
+ },
149
+ });
150
+ }
151
+
152
+ async getEmailNotifications(userId: string) {
153
+ return this._user.model.user.findUnique({
154
+ where: {
155
+ id: userId,
156
+ },
157
+ select: {
158
+ sendSuccessEmails: true,
159
+ sendFailureEmails: true,
160
+ sendStreakEmails: true,
161
+ },
162
+ });
163
+ }
164
+
165
+ async updateEmailNotifications(userId: string, body: EmailNotificationsDto) {
166
+ await this._user.model.user.update({
167
+ where: {
168
+ id: userId,
169
+ },
170
+ data: {
171
+ sendSuccessEmails: body.sendSuccessEmails,
172
+ sendFailureEmails: body.sendFailureEmails,
173
+ sendStreakEmails: body.sendStreakEmails,
174
+ },
175
+ });
176
+ }
177
+ }
libraries/nestjs-libraries/src/database/prisma/users/users.service.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
3
+ import { Provider } from '@prisma/client';
4
+ import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto';
5
+ import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto';
6
+ import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository';
7
+
8
+ @Injectable()
9
+ export class UsersService {
10
+ constructor(
11
+ private _usersRepository: UsersRepository,
12
+ private _organizationRepository: OrganizationRepository
13
+ ) {}
14
+
15
+ getUserByEmail(email: string) {
16
+ return this._usersRepository.getUserByEmail(email);
17
+ }
18
+
19
+ getUserById(id: string) {
20
+ return this._usersRepository.getUserById(id);
21
+ }
22
+
23
+ getImpersonateUser(name: string) {
24
+ return this._organizationRepository.getImpersonateUser(name);
25
+ }
26
+
27
+ getUserByProvider(providerId: string, provider: Provider) {
28
+ return this._usersRepository.getUserByProvider(providerId, provider);
29
+ }
30
+
31
+ activateUser(id: string) {
32
+ return this._usersRepository.activateUser(id);
33
+ }
34
+
35
+ updatePassword(id: string, password: string) {
36
+ return this._usersRepository.updatePassword(id, password);
37
+ }
38
+
39
+ getPersonal(userId: string) {
40
+ return this._usersRepository.getPersonal(userId);
41
+ }
42
+
43
+ changePersonal(userId: string, body: UserDetailDto) {
44
+ return this._usersRepository.changePersonal(userId, body);
45
+ }
46
+
47
+ getEmailNotifications(userId: string) {
48
+ return this._usersRepository.getEmailNotifications(userId);
49
+ }
50
+
51
+ updateEmailNotifications(userId: string, body: EmailNotificationsDto) {
52
+ return this._usersRepository.updateEmailNotifications(userId, body);
53
+ }
54
+ }
libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.repository.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { WebhooksDto } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ @Injectable()
7
+ export class WebhooksRepository {
8
+ constructor(private _webhooks: PrismaRepository<'webhooks'>) {}
9
+
10
+ getTotal(orgId: string) {
11
+ return this._webhooks.model.webhooks.count({
12
+ where: {
13
+ organizationId: orgId,
14
+ deletedAt: null,
15
+ },
16
+ });
17
+ }
18
+
19
+ getWebhooks(orgId: string) {
20
+ return this._webhooks.model.webhooks.findMany({
21
+ where: {
22
+ organizationId: orgId,
23
+ deletedAt: null,
24
+ },
25
+ include: {
26
+ integrations: {
27
+ select: {
28
+ integration: {
29
+ select: {
30
+ id: true,
31
+ picture: true,
32
+ name: true,
33
+ },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ });
39
+ }
40
+
41
+ deleteWebhook(orgId: string, id: string) {
42
+ return this._webhooks.model.webhooks.update({
43
+ where: {
44
+ id,
45
+ organizationId: orgId,
46
+ },
47
+ data: {
48
+ deletedAt: new Date(),
49
+ },
50
+ });
51
+ }
52
+
53
+ async createWebhook(orgId: string, body: WebhooksDto) {
54
+ const { id } = await this._webhooks.model.webhooks.upsert({
55
+ where: {
56
+ id: body.id || uuidv4(),
57
+ organizationId: orgId,
58
+ },
59
+ create: {
60
+ organizationId: orgId,
61
+ url: body.url,
62
+ name: body.name,
63
+ },
64
+ update: {
65
+ url: body.url,
66
+ name: body.name,
67
+ },
68
+ });
69
+
70
+ await this._webhooks.model.webhooks.update({
71
+ where: {
72
+ id,
73
+ organizationId: orgId,
74
+ },
75
+ data: {
76
+ integrations: {
77
+ deleteMany: {},
78
+ create: body.integrations.map((integration) => ({
79
+ integrationId: integration.id,
80
+ })),
81
+ },
82
+ },
83
+ });
84
+
85
+ return { id };
86
+ }
87
+ }
libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.service.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.repository';
3
+ import { WebhooksDto } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
4
+
5
+ @Injectable()
6
+ export class WebhooksService {
7
+ constructor(private _webhooksRepository: WebhooksRepository) {}
8
+
9
+ getTotal(orgId: string) {
10
+ return this._webhooksRepository.getTotal(orgId);
11
+ }
12
+
13
+ getWebhooks(orgId: string) {
14
+ return this._webhooksRepository.getWebhooks(orgId);
15
+ }
16
+
17
+ createWebhook(orgId: string, body: WebhooksDto) {
18
+ return this._webhooksRepository.createWebhook(orgId, body);
19
+ }
20
+
21
+ deleteWebhook(orgId: string, id: string) {
22
+ return this._webhooksRepository.deleteWebhook(orgId, id);
23
+ }
24
+ }
libraries/nestjs-libraries/src/dtos/agencies/create.agency.dto.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ArrayMaxSize,
3
+ ArrayMinSize,
4
+ IsDefined,
5
+ IsIn,
6
+ IsOptional,
7
+ IsString,
8
+ IsUrl,
9
+ MinLength,
10
+ ValidateIf,
11
+ } from 'class-validator';
12
+ import { Type } from 'class-transformer';
13
+
14
+ export class CreateAgencyLogoDto {
15
+ @IsString()
16
+ @IsDefined()
17
+ id: string;
18
+
19
+ path: string;
20
+ }
21
+ export class CreateAgencyDto {
22
+ @IsString()
23
+ @MinLength(3)
24
+ name: string;
25
+
26
+ @IsUrl()
27
+ @IsDefined()
28
+ website: string;
29
+
30
+ @IsUrl()
31
+ @ValidateIf((o) => o.facebook)
32
+ facebook: string;
33
+
34
+ @IsString()
35
+ @IsOptional()
36
+ instagram: string;
37
+
38
+ @IsString()
39
+ @IsOptional()
40
+ twitter: string;
41
+
42
+ @IsUrl()
43
+ @ValidateIf((o) => o.linkedIn)
44
+ linkedIn: string;
45
+
46
+ @IsUrl()
47
+ @ValidateIf((o) => o.youtube)
48
+ youtube: string;
49
+
50
+ @IsString()
51
+ @IsOptional()
52
+ tiktok: string;
53
+
54
+ @Type(() => CreateAgencyLogoDto)
55
+ logo: CreateAgencyLogoDto;
56
+
57
+ @IsString()
58
+ shortDescription: string;
59
+
60
+ @IsString()
61
+ description: string;
62
+
63
+ @IsString({
64
+ each: true,
65
+ })
66
+ @ArrayMinSize(1)
67
+ @ArrayMaxSize(3)
68
+ @IsIn(
69
+ [
70
+ 'Real Estate',
71
+ 'Fashion',
72
+ 'Health and Fitness',
73
+ 'Beauty',
74
+ 'Travel',
75
+ 'Food',
76
+ 'Tech',
77
+ 'Gaming',
78
+ 'Parenting',
79
+ 'Education',
80
+ 'Business',
81
+ 'Finance',
82
+ 'DIY',
83
+ 'Pets',
84
+ 'Lifestyle',
85
+ 'Sports',
86
+ 'Entertainment',
87
+ 'Art',
88
+ 'Photography',
89
+ 'Sustainability',
90
+ ],
91
+ {
92
+ each: true,
93
+ }
94
+ )
95
+ niches: string[];
96
+ }
libraries/nestjs-libraries/src/dtos/analytics/stars.list.dto.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
2
+
3
+ export class StarsListDto {
4
+ @IsNumber()
5
+ @IsDefined()
6
+ page: number;
7
+
8
+ @IsOptional()
9
+ @IsIn(['login', 'totalStars', 'stars', 'date', 'forks', 'totalForks'])
10
+ key: 'login' | 'date' | 'stars' | 'totalStars';
11
+
12
+ @IsOptional()
13
+ @IsIn(['desc', 'asc'])
14
+ state: 'desc' | 'asc';
15
+ }
libraries/nestjs-libraries/src/dtos/announcements/announcements.dto.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IsDefined, IsIn, IsOptional, IsString } from 'class-validator';
2
+
3
+ export class AnnouncementDto {
4
+ @IsString()
5
+ @IsDefined()
6
+ title: string;
7
+
8
+ @IsString()
9
+ @IsDefined()
10
+ description: string;
11
+
12
+ @IsOptional()
13
+ @IsString()
14
+ @IsIn(['INFO', 'WARNING', 'ERROR'])
15
+ color?: string;
16
+ }
libraries/nestjs-libraries/src/dtos/auth/create.org.user.dto.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ IsDefined,
3
+ IsEmail,
4
+ IsString,
5
+ MaxLength,
6
+ MinLength,
7
+ ValidateIf,
8
+ } from 'class-validator';
9
+ import { Provider } from '@prisma/client';
10
+
11
+ export class CreateOrgUserDto {
12
+ @IsString()
13
+ @MinLength(3)
14
+ @MaxLength(64)
15
+ @IsDefined()
16
+ @ValidateIf((o) => !o.providerToken)
17
+ password: string;
18
+
19
+ @IsString()
20
+ @IsDefined()
21
+ provider: Provider;
22
+
23
+ @IsString()
24
+ @IsDefined()
25
+ @ValidateIf((o) => !o.password)
26
+ providerToken: string;
27
+
28
+ @IsEmail()
29
+ @IsDefined()
30
+ @ValidateIf((o) => !o.providerToken)
31
+ email: string;
32
+
33
+ @IsString()
34
+ @IsDefined()
35
+ @MinLength(3)
36
+ @MaxLength(128)
37
+ company: string;
38
+
39
+ datafast_visitor_id: string;
40
+ }
libraries/nestjs-libraries/src/dtos/auth/forgot-return.password.dto.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ IsDefined,
3
+ IsIn,
4
+ IsString,
5
+ MinLength,
6
+ ValidateIf,
7
+ } from 'class-validator';
8
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
9
+
10
+ export class ForgotReturnPasswordDto {
11
+ @IsString()
12
+ @IsDefined()
13
+ @MinLength(3)
14
+ password: string;
15
+
16
+ @IsString()
17
+ @IsDefined()
18
+ @IsIn([makeId(10)], {
19
+ message: 'Passwords do not match',
20
+ })
21
+ @ValidateIf((o) => o.password !== o.repeatPassword)
22
+ repeatPassword: string;
23
+
24
+ @IsString()
25
+ @IsDefined()
26
+ @MinLength(5)
27
+ token: string;
28
+ }
libraries/nestjs-libraries/src/dtos/auth/forgot.password.dto.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { IsDefined, IsEmail, IsString } from 'class-validator';
2
+
3
+ export class ForgotPasswordDto {
4
+ @IsString()
5
+ @IsDefined()
6
+ @IsEmail()
7
+ email: string;
8
+ }
libraries/nestjs-libraries/src/dtos/auth/login.user.dto.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ IsDefined,
3
+ IsEmail,
4
+ IsString,
5
+ MinLength,
6
+ ValidateIf,
7
+ } from 'class-validator';
8
+ import { Provider } from '@prisma/client';
9
+
10
+ export class LoginUserDto {
11
+ @IsString()
12
+ @IsDefined()
13
+ @ValidateIf((o) => !o.providerToken)
14
+ @MinLength(3)
15
+ password: string;
16
+
17
+ @IsString()
18
+ @IsDefined()
19
+ provider: Provider;
20
+
21
+ @IsString()
22
+ @IsDefined()
23
+ @ValidateIf((o) => !o.password)
24
+ providerToken: string;
25
+
26
+ @IsEmail()
27
+ @IsDefined()
28
+ email: string;
29
+
30
+ datafast_visitor_id: string;
31
+ }