@m5kdev/backend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +621 -0
- package/README.md +22 -0
- package/package.json +205 -0
- package/src/lib/posthog.ts +5 -0
- package/src/lib/sentry.ts +8 -0
- package/src/modules/access/access.repository.ts +36 -0
- package/src/modules/access/access.service.ts +81 -0
- package/src/modules/access/access.test.ts +216 -0
- package/src/modules/access/access.utils.ts +46 -0
- package/src/modules/ai/ai.db.ts +38 -0
- package/src/modules/ai/ai.prompt.ts +47 -0
- package/src/modules/ai/ai.repository.ts +53 -0
- package/src/modules/ai/ai.router.ts +148 -0
- package/src/modules/ai/ai.service.ts +310 -0
- package/src/modules/ai/ai.trpc.ts +22 -0
- package/src/modules/ai/ideogram/ideogram.constants.ts +170 -0
- package/src/modules/ai/ideogram/ideogram.dto.ts +64 -0
- package/src/modules/ai/ideogram/ideogram.prompt.ts +858 -0
- package/src/modules/ai/ideogram/ideogram.repository.ts +39 -0
- package/src/modules/ai/ideogram/ideogram.service.ts +14 -0
- package/src/modules/auth/auth.db.ts +224 -0
- package/src/modules/auth/auth.dto.ts +47 -0
- package/src/modules/auth/auth.lib.ts +349 -0
- package/src/modules/auth/auth.middleware.ts +62 -0
- package/src/modules/auth/auth.repository.ts +672 -0
- package/src/modules/auth/auth.service.ts +261 -0
- package/src/modules/auth/auth.trpc.ts +208 -0
- package/src/modules/auth/auth.utils.ts +117 -0
- package/src/modules/base/base.abstract.ts +62 -0
- package/src/modules/base/base.dto.ts +206 -0
- package/src/modules/base/base.grants.test.ts +861 -0
- package/src/modules/base/base.grants.ts +199 -0
- package/src/modules/base/base.repository.ts +433 -0
- package/src/modules/base/base.service.ts +154 -0
- package/src/modules/base/base.types.ts +7 -0
- package/src/modules/billing/billing.db.ts +27 -0
- package/src/modules/billing/billing.repository.ts +328 -0
- package/src/modules/billing/billing.router.ts +77 -0
- package/src/modules/billing/billing.service.ts +177 -0
- package/src/modules/billing/billing.trpc.ts +17 -0
- package/src/modules/clay/clay.repository.ts +29 -0
- package/src/modules/clay/clay.service.ts +61 -0
- package/src/modules/connect/connect.db.ts +32 -0
- package/src/modules/connect/connect.dto.ts +44 -0
- package/src/modules/connect/connect.linkedin.ts +70 -0
- package/src/modules/connect/connect.oauth.ts +288 -0
- package/src/modules/connect/connect.repository.ts +65 -0
- package/src/modules/connect/connect.router.ts +76 -0
- package/src/modules/connect/connect.service.ts +171 -0
- package/src/modules/connect/connect.trpc.ts +26 -0
- package/src/modules/connect/connect.types.ts +27 -0
- package/src/modules/crypto/crypto.db.ts +15 -0
- package/src/modules/crypto/crypto.repository.ts +13 -0
- package/src/modules/crypto/crypto.service.ts +57 -0
- package/src/modules/email/email.service.ts +222 -0
- package/src/modules/file/file.repository.ts +95 -0
- package/src/modules/file/file.router.ts +108 -0
- package/src/modules/file/file.service.ts +186 -0
- package/src/modules/recurrence/recurrence.db.ts +79 -0
- package/src/modules/recurrence/recurrence.repository.ts +70 -0
- package/src/modules/recurrence/recurrence.service.ts +105 -0
- package/src/modules/recurrence/recurrence.trpc.ts +82 -0
- package/src/modules/social/social.dto.ts +22 -0
- package/src/modules/social/social.linkedin.test.ts +277 -0
- package/src/modules/social/social.linkedin.ts +593 -0
- package/src/modules/social/social.service.ts +112 -0
- package/src/modules/social/social.types.ts +43 -0
- package/src/modules/tag/tag.db.ts +41 -0
- package/src/modules/tag/tag.dto.ts +18 -0
- package/src/modules/tag/tag.repository.ts +222 -0
- package/src/modules/tag/tag.service.ts +48 -0
- package/src/modules/tag/tag.trpc.ts +62 -0
- package/src/modules/uploads/0581796b-8845-420d-bd95-cd7de79f6d37.webm +0 -0
- package/src/modules/uploads/33b1e649-6727-4bd0-94d0-a0b363646865.webm +0 -0
- package/src/modules/uploads/49a8c4c0-54d7-4c94-bef4-c93c029f9ed0.webm +0 -0
- package/src/modules/uploads/50e31e38-a2f0-47ca-8b7d-2d7fcad9267d.webm +0 -0
- package/src/modules/uploads/72ac8cf9-c3a7-4cd8-8a78-6d8e137a4c7e.webm +0 -0
- package/src/modules/uploads/75293649-d966-46cd-a675-67518958ae9c.png +0 -0
- package/src/modules/uploads/88b7b867-ce15-4891-bf73-81305a7de1f7.wav +0 -0
- package/src/modules/uploads/a5d6fee8-6a59-42c6-9d4a-ac8a3c5e7245.webm +0 -0
- package/src/modules/uploads/c13a9785-ca5a-4983-af30-b338ed76d370.webm +0 -0
- package/src/modules/uploads/caa1a5a7-71ba-4381-902d-7e2cafdf6dcb.webm +0 -0
- package/src/modules/uploads/cbeb0b81-374d-445b-914b-40ace7c8e031.webm +0 -0
- package/src/modules/uploads/d626aa82-b10f-493f-aee7-87bfb3361dfc.webm +0 -0
- package/src/modules/uploads/d7de4c16-de0c-495d-9612-e72260a6ecca.png +0 -0
- package/src/modules/uploads/e532e38a-6421-400e-8a5f-8e7bc8ce411b.wav +0 -0
- package/src/modules/uploads/e86ec867-6adf-4c51-84e0-00b0836625e8.webm +0 -0
- package/src/modules/utils/applyPagination.ts +13 -0
- package/src/modules/utils/applySorting.ts +21 -0
- package/src/modules/utils/getConditionsFromFilters.ts +216 -0
- package/src/modules/video/video.service.ts +89 -0
- package/src/modules/webhook/webhook.constants.ts +9 -0
- package/src/modules/webhook/webhook.db.ts +15 -0
- package/src/modules/webhook/webhook.dto.ts +9 -0
- package/src/modules/webhook/webhook.repository.ts +68 -0
- package/src/modules/webhook/webhook.router.ts +29 -0
- package/src/modules/webhook/webhook.service.ts +78 -0
- package/src/modules/workflow/workflow.db.ts +29 -0
- package/src/modules/workflow/workflow.repository.ts +171 -0
- package/src/modules/workflow/workflow.service.ts +56 -0
- package/src/modules/workflow/workflow.trpc.ts +26 -0
- package/src/modules/workflow/workflow.types.ts +30 -0
- package/src/modules/workflow/workflow.utils.ts +259 -0
- package/src/test/stubs/utils.ts +2 -0
- package/src/trpc/context.ts +21 -0
- package/src/trpc/index.ts +3 -0
- package/src/trpc/procedures.ts +43 -0
- package/src/trpc/utils.ts +20 -0
- package/src/types.ts +22 -0
- package/src/utils/errors.ts +148 -0
- package/src/utils/logger.ts +8 -0
- package/src/utils/posthog.ts +43 -0
- package/src/utils/types.ts +5 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ConnectRow } from "#modules/connect/connect.repository";
|
|
2
|
+
import type { FileService } from "#modules/file/file.service";
|
|
3
|
+
|
|
4
|
+
export type SocialMediaType = "image" | "video" | "document";
|
|
5
|
+
|
|
6
|
+
export interface SocialMediaDescriptor {
|
|
7
|
+
readonly s3Path: string;
|
|
8
|
+
readonly mediaType?: SocialMediaType;
|
|
9
|
+
readonly title?: string;
|
|
10
|
+
readonly description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SocialVisibility = "PUBLIC" | "CONNECTIONS";
|
|
14
|
+
|
|
15
|
+
export interface SocialPostPayload {
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly media?: readonly SocialMediaDescriptor[];
|
|
18
|
+
readonly visibility: SocialVisibility;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SocialProviderContext {
|
|
22
|
+
readonly userId: string;
|
|
23
|
+
readonly connection: ConnectRow;
|
|
24
|
+
readonly accessToken: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SocialProviderDeps {
|
|
28
|
+
readonly fileService: FileService;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SocialPostResult {
|
|
32
|
+
readonly shareUrn?: string;
|
|
33
|
+
readonly rawResponse?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SocialProvider {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
post(options: {
|
|
39
|
+
deps: SocialProviderDeps;
|
|
40
|
+
context: SocialProviderContext;
|
|
41
|
+
payload: SocialPostPayload;
|
|
42
|
+
}): Promise<SocialPostResult>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AnySQLiteColumn, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { organizations, teams, users } from "#modules/auth/auth.db";
|
|
4
|
+
|
|
5
|
+
export const tags = sqliteTable("tags", {
|
|
6
|
+
id: text("id").primaryKey().$default(uuidv4),
|
|
7
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
8
|
+
.notNull()
|
|
9
|
+
.$default(() => new Date()),
|
|
10
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
11
|
+
deletedAt: integer("deleted_at", { mode: "timestamp" }),
|
|
12
|
+
userId: text("user_id")
|
|
13
|
+
.notNull()
|
|
14
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
15
|
+
organizationId: text("organization_id").references(() => organizations.id, {
|
|
16
|
+
onDelete: "cascade",
|
|
17
|
+
}),
|
|
18
|
+
teamId: text("team_id").references(() => teams.id, { onDelete: "cascade" }),
|
|
19
|
+
name: text("name").notNull(),
|
|
20
|
+
color: text("color"),
|
|
21
|
+
type: text("type"),
|
|
22
|
+
isEnabled: integer("is_enabled", { mode: "boolean" }).notNull().default(true),
|
|
23
|
+
parentId: text("parent_id").references((): AnySQLiteColumn => tags.id, { onDelete: "set null" }),
|
|
24
|
+
assignableTo: text("assignable_to", {
|
|
25
|
+
mode: "json",
|
|
26
|
+
})
|
|
27
|
+
.notNull()
|
|
28
|
+
.$type<string[]>(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const taggings = sqliteTable("taggings", {
|
|
32
|
+
id: text("id").primaryKey().$default(uuidv4),
|
|
33
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
34
|
+
.notNull()
|
|
35
|
+
.$default(() => new Date()),
|
|
36
|
+
tagId: text("tag_id")
|
|
37
|
+
.notNull()
|
|
38
|
+
.references(() => tags.id),
|
|
39
|
+
resourceType: text("resource_type").notNull(), // e.g., "post", "image"
|
|
40
|
+
resourceId: text("resource_id").notNull(), // id in the resource table
|
|
41
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Result } from "neverthrow";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import { createSelectDTO } from "#modules/base/base.dto";
|
|
4
|
+
import { taggings, tags } from "#modules/tag/tag.db";
|
|
5
|
+
import type { ServerError } from "#utils/errors";
|
|
6
|
+
|
|
7
|
+
export const tagsSelectDTO = createSelectDTO(tags);
|
|
8
|
+
export const taggingsSelectDTO = createSelectDTO(taggings);
|
|
9
|
+
|
|
10
|
+
export const tagsSelectOutput = tagsSelectDTO.schema;
|
|
11
|
+
export const taggingsSelectOutput = taggingsSelectDTO.schema;
|
|
12
|
+
|
|
13
|
+
export type TagSelectOutputResult = Result<z.infer<typeof tagsSelectOutput>, ServerError>;
|
|
14
|
+
export type TaggingSelectOutputResult = Result<z.infer<typeof taggingsSelectOutput>, ServerError>;
|
|
15
|
+
export type TaggingSelectOutputResults = Result<
|
|
16
|
+
z.infer<typeof taggingsSelectOutput>[],
|
|
17
|
+
ServerError
|
|
18
|
+
>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TaggingSchema,
|
|
3
|
+
TagLinkSchema,
|
|
4
|
+
TagListInputSchema,
|
|
5
|
+
TagListSchema,
|
|
6
|
+
TagSchema,
|
|
7
|
+
} from "@m5kdev/commons/modules/tag/tag.schema";
|
|
8
|
+
import { and, count, eq, inArray } from "drizzle-orm";
|
|
9
|
+
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
10
|
+
import { err, ok } from "neverthrow";
|
|
11
|
+
import type { ServerResultAsync } from "#modules/base/base.dto";
|
|
12
|
+
import { BaseTableRepository } from "#modules/base/base.repository";
|
|
13
|
+
import * as tag from "#modules/tag/tag.db";
|
|
14
|
+
import type { TaggingSelectOutputResult, TagSelectOutputResult } from "#modules/tag/tag.dto";
|
|
15
|
+
|
|
16
|
+
const schema = { ...tag };
|
|
17
|
+
type Schema = typeof schema;
|
|
18
|
+
type Orm = LibSQLDatabase<Schema>;
|
|
19
|
+
|
|
20
|
+
export class TagRepository extends BaseTableRepository<
|
|
21
|
+
Orm,
|
|
22
|
+
Schema,
|
|
23
|
+
Record<string, never>,
|
|
24
|
+
Schema["tags"]
|
|
25
|
+
> {
|
|
26
|
+
async link(
|
|
27
|
+
{ userId, ...data }: TagLinkSchema & { userId: string },
|
|
28
|
+
tx?: Orm
|
|
29
|
+
): Promise<TaggingSelectOutputResult> {
|
|
30
|
+
return this.throwableAsync(async () => {
|
|
31
|
+
const db = tx ?? this.orm;
|
|
32
|
+
const [foundTag] = await db
|
|
33
|
+
.select({ id: this.schema.tags.id })
|
|
34
|
+
.from(this.schema.tags)
|
|
35
|
+
.where(and(eq(this.schema.tags.id, data.tagId), eq(this.schema.tags.userId, userId)))
|
|
36
|
+
.limit(1);
|
|
37
|
+
if (!foundTag) return this.error("FORBIDDEN");
|
|
38
|
+
|
|
39
|
+
const [tagging] = await db
|
|
40
|
+
.insert(this.schema.taggings)
|
|
41
|
+
.values({ ...data, tagId: foundTag.id })
|
|
42
|
+
.returning();
|
|
43
|
+
if (!tagging) return this.error("NOT_FOUND");
|
|
44
|
+
return ok(tagging);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async linkBulk(data: TagLinkSchema[], tx?: Orm): ServerResultAsync<TagSchema[]> {
|
|
49
|
+
return this.throwableAsync(async () => {
|
|
50
|
+
const db = tx ?? this.orm;
|
|
51
|
+
await db.insert(this.schema.taggings).values(data);
|
|
52
|
+
const tags = await db
|
|
53
|
+
.select()
|
|
54
|
+
.from(this.schema.tags)
|
|
55
|
+
.where(
|
|
56
|
+
inArray(
|
|
57
|
+
this.schema.tags.id,
|
|
58
|
+
data.map((tag) => tag.tagId)
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
return ok(tags);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async set(data: TagLinkSchema[], tx?: Orm): ServerResultAsync<TagSchema[]> {
|
|
66
|
+
return this.throwableAsync(async () => {
|
|
67
|
+
const db = tx ?? this.orm;
|
|
68
|
+
const result = await db.transaction(async (trx) => {
|
|
69
|
+
// FIXME: We are assuming that all resourceIds are the same, this is not a good assumption.
|
|
70
|
+
await trx
|
|
71
|
+
.delete(this.schema.taggings)
|
|
72
|
+
.where(eq(this.schema.taggings.resourceId, data[0].resourceId));
|
|
73
|
+
await db.insert(this.schema.taggings).values(data);
|
|
74
|
+
const tags = await db
|
|
75
|
+
.select()
|
|
76
|
+
.from(this.schema.tags)
|
|
77
|
+
.where(
|
|
78
|
+
inArray(
|
|
79
|
+
this.schema.tags.id,
|
|
80
|
+
data.map((tag) => tag.tagId)
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
return tags;
|
|
84
|
+
});
|
|
85
|
+
return ok(result);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async unlink(
|
|
90
|
+
{ userId, ...data }: TagLinkSchema & { userId: string },
|
|
91
|
+
tx?: Orm
|
|
92
|
+
): Promise<TagSelectOutputResult> {
|
|
93
|
+
return this.throwableAsync(async () => {
|
|
94
|
+
const db = tx ?? this.orm;
|
|
95
|
+
const [foundTag] = await db
|
|
96
|
+
.select()
|
|
97
|
+
.from(this.schema.tags)
|
|
98
|
+
.where(and(eq(this.schema.tags.id, data.tagId), eq(this.schema.tags.userId, userId)))
|
|
99
|
+
.limit(1);
|
|
100
|
+
if (!foundTag) return this.error("FORBIDDEN");
|
|
101
|
+
|
|
102
|
+
await db
|
|
103
|
+
.delete(this.schema.taggings)
|
|
104
|
+
.where(
|
|
105
|
+
and(
|
|
106
|
+
eq(this.schema.taggings.tagId, data.tagId),
|
|
107
|
+
eq(this.schema.taggings.resourceId, data.resourceId),
|
|
108
|
+
eq(this.schema.taggings.resourceType, data.resourceType)
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return ok(foundTag);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async findTagsForResources(
|
|
117
|
+
data: { resourceType: string; resourceIds: readonly string[] },
|
|
118
|
+
tx?: Orm
|
|
119
|
+
): ServerResultAsync<Record<string, TagSchema[]>> {
|
|
120
|
+
return this.throwableAsync(async () => {
|
|
121
|
+
const db = tx ?? this.orm;
|
|
122
|
+
if (data.resourceIds.length === 0) return ok({});
|
|
123
|
+
|
|
124
|
+
const taggings = await db
|
|
125
|
+
.select({
|
|
126
|
+
resourceId: this.schema.taggings.resourceId,
|
|
127
|
+
tagId: this.schema.taggings.tagId,
|
|
128
|
+
})
|
|
129
|
+
.from(this.schema.taggings)
|
|
130
|
+
.where(
|
|
131
|
+
and(
|
|
132
|
+
eq(this.schema.taggings.resourceType, data.resourceType),
|
|
133
|
+
inArray(this.schema.taggings.resourceId, data.resourceIds as string[])
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (taggings.length === 0) return ok({});
|
|
138
|
+
|
|
139
|
+
const tagIds = Array.from(new Set(taggings.map((tagging) => tagging.tagId)));
|
|
140
|
+
const tags = await db
|
|
141
|
+
.select()
|
|
142
|
+
.from(this.schema.tags)
|
|
143
|
+
.where(inArray(this.schema.tags.id, tagIds));
|
|
144
|
+
|
|
145
|
+
const tagById = tags.reduce<Record<string, TagSchema>>((acc, tagRow) => {
|
|
146
|
+
acc[tagRow.id] = tagRow;
|
|
147
|
+
return acc;
|
|
148
|
+
}, {});
|
|
149
|
+
|
|
150
|
+
const grouped = taggings.reduce<Record<string, TagSchema[]>>((acc, tagging) => {
|
|
151
|
+
const tagRow = tagById[tagging.tagId];
|
|
152
|
+
if (!tagRow) return acc;
|
|
153
|
+
const existing = acc[tagging.resourceId] ?? [];
|
|
154
|
+
acc[tagging.resourceId] = [...existing, tagRow];
|
|
155
|
+
return acc;
|
|
156
|
+
}, {});
|
|
157
|
+
|
|
158
|
+
return ok(grouped);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async attachTagsToResources<TRow extends { id: string }>(
|
|
163
|
+
resourceType: string,
|
|
164
|
+
rows: readonly TRow[],
|
|
165
|
+
tx?: Orm
|
|
166
|
+
): ServerResultAsync<Array<TRow & { tags: TagSchema[] }>> {
|
|
167
|
+
return this.throwableAsync(async () => {
|
|
168
|
+
if (rows.length === 0) return ok([]);
|
|
169
|
+
const tagsResult = await this.findTagsForResources(
|
|
170
|
+
{ resourceType, resourceIds: rows.map((row) => row.id) },
|
|
171
|
+
tx
|
|
172
|
+
);
|
|
173
|
+
if (tagsResult.isErr()) return err(tagsResult.error);
|
|
174
|
+
const tagsByResource = tagsResult.value;
|
|
175
|
+
const withTags = rows.map((row) => ({
|
|
176
|
+
...row,
|
|
177
|
+
tags: tagsByResource[row.id] ?? [],
|
|
178
|
+
}));
|
|
179
|
+
return ok(withTags);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async listTaggings(
|
|
184
|
+
input: { resourceType: string; resourceIds?: readonly string[] },
|
|
185
|
+
tx?: Orm
|
|
186
|
+
): ServerResultAsync<TaggingSchema[]> {
|
|
187
|
+
return this.throwableAsync(async () => {
|
|
188
|
+
const db = tx ?? this.orm;
|
|
189
|
+
const filters = [eq(this.schema.taggings.resourceType, input.resourceType)];
|
|
190
|
+
if (input.resourceIds?.length) {
|
|
191
|
+
filters.push(inArray(this.schema.taggings.resourceId, input.resourceIds as string[]));
|
|
192
|
+
}
|
|
193
|
+
const rows = await db
|
|
194
|
+
.select()
|
|
195
|
+
.from(this.schema.taggings)
|
|
196
|
+
.where(and(...filters));
|
|
197
|
+
return ok(rows);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async list(
|
|
202
|
+
input: (TagListInputSchema & TagListSchema) | undefined,
|
|
203
|
+
tx?: Orm
|
|
204
|
+
): ServerResultAsync<{ rows: TagSchema[]; total: number }> {
|
|
205
|
+
return this.throwableAsync(async () => {
|
|
206
|
+
const db = tx ?? this.orm;
|
|
207
|
+
const conditions = this.getConditionBuilder(this.table);
|
|
208
|
+
conditions.applyFilters(input);
|
|
209
|
+
if (input?.assignableTo) {
|
|
210
|
+
conditions.push(this.helpers.arrayContains(this.table.assignableTo, [input.assignableTo]));
|
|
211
|
+
}
|
|
212
|
+
const whereClause = conditions.join();
|
|
213
|
+
const rowsQuery = this.withSortingAndPagination(
|
|
214
|
+
db.select().from(this.table).where(whereClause),
|
|
215
|
+
input || {}
|
|
216
|
+
);
|
|
217
|
+
const countQuery = db.select({ count: count() }).from(this.table).where(whereClause);
|
|
218
|
+
const [rows, [totalResult]] = await Promise.all([rowsQuery, countQuery]);
|
|
219
|
+
return ok({ rows, total: totalResult?.count ?? 0 });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TagCreateSchema,
|
|
3
|
+
TagLinkSchema,
|
|
4
|
+
TagListInputSchema,
|
|
5
|
+
TagListOutputSchema,
|
|
6
|
+
TagListSchema,
|
|
7
|
+
TagSchema,
|
|
8
|
+
TagUpdateSchema,
|
|
9
|
+
} from "@m5kdev/commons/modules/tag/tag.schema";
|
|
10
|
+
import type { User } from "#modules/auth/auth.lib";
|
|
11
|
+
import type { ServerResultAsync } from "#modules/base/base.dto";
|
|
12
|
+
import { BaseService } from "#modules/base/base.service";
|
|
13
|
+
import type { TaggingSelectOutputResult, TagSelectOutputResult } from "#modules/tag/tag.dto";
|
|
14
|
+
import type { TagRepository } from "#modules/tag/tag.repository";
|
|
15
|
+
|
|
16
|
+
export class TagService extends BaseService<{ tag: TagRepository }, Record<string, never>> {
|
|
17
|
+
async list(input: TagListInputSchema & TagListSchema): ServerResultAsync<TagListOutputSchema> {
|
|
18
|
+
return this.repository.tag.list(input);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async listTaggings(input: { resourceType: string; resourceIds?: readonly string[] }) {
|
|
22
|
+
return this.repository.tag.listTaggings(input);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async create(data: TagCreateSchema, { user }: { user: User }): Promise<TagSelectOutputResult> {
|
|
26
|
+
return this.repository.tag.create({ ...data, userId: user.id });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async update(data: TagUpdateSchema, { user }: { user: User }): Promise<TagSelectOutputResult> {
|
|
30
|
+
return this.repository.tag.update({ ...data, userId: user.id });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async link(data: TagLinkSchema, { user }: { user: User }): Promise<TaggingSelectOutputResult> {
|
|
34
|
+
return this.repository.tag.link({ ...data, userId: user.id });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async linkBulk(data: TagLinkSchema[]): ServerResultAsync<TagSchema[]> {
|
|
38
|
+
return this.repository.tag.linkBulk(data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async set(data: TagLinkSchema[]): ServerResultAsync<TagSchema[]> {
|
|
42
|
+
return this.repository.tag.set(data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async unlink(data: TagLinkSchema, { user }: { user: User }): Promise<TagSelectOutputResult> {
|
|
46
|
+
return this.repository.tag.unlink({ ...data, userId: user.id });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tagCreateSchema,
|
|
3
|
+
taggingSchema,
|
|
4
|
+
tagLinkSchema,
|
|
5
|
+
tagListInputSchema,
|
|
6
|
+
tagListOutputSchema,
|
|
7
|
+
tagListSchema,
|
|
8
|
+
tagUpdateSchema,
|
|
9
|
+
} from "@m5kdev/commons/modules/tag/tag.schema";
|
|
10
|
+
import { taggingsSelectOutput, tagsSelectOutput } from "#modules/tag/tag.dto";
|
|
11
|
+
import type { TagService } from "#modules/tag/tag.service";
|
|
12
|
+
import { handleTRPCResult, procedure, router } from "#trpc";
|
|
13
|
+
|
|
14
|
+
export function createTagTRPC(tagService: TagService) {
|
|
15
|
+
const tagListInput = tagListInputSchema.extend({
|
|
16
|
+
assignableTo: tagListSchema.shape.assignableTo,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return router({
|
|
20
|
+
list: procedure
|
|
21
|
+
.input(tagListInput)
|
|
22
|
+
.output(tagListOutputSchema)
|
|
23
|
+
.query(async ({ input }) => handleTRPCResult(await tagService.list(input))),
|
|
24
|
+
|
|
25
|
+
listTaggings: procedure
|
|
26
|
+
.input(
|
|
27
|
+
taggingSchema
|
|
28
|
+
.pick({ resourceType: true })
|
|
29
|
+
.extend({ resourceIds: tagListInputSchema.shape.filters.optional() })
|
|
30
|
+
)
|
|
31
|
+
.output(taggingsSelectOutput.array())
|
|
32
|
+
.query(async ({ input }) => handleTRPCResult(await tagService.listTaggings(input as any))),
|
|
33
|
+
|
|
34
|
+
create: procedure
|
|
35
|
+
.input(tagCreateSchema)
|
|
36
|
+
.output(tagsSelectOutput)
|
|
37
|
+
.mutation(async ({ ctx, input }) => {
|
|
38
|
+
return handleTRPCResult(await tagService.create(input, ctx));
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
update: procedure
|
|
42
|
+
.input(tagUpdateSchema)
|
|
43
|
+
.output(tagsSelectOutput)
|
|
44
|
+
.mutation(async ({ ctx, input }) => {
|
|
45
|
+
return handleTRPCResult(await tagService.update(input, ctx));
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
link: procedure
|
|
49
|
+
.input(tagLinkSchema)
|
|
50
|
+
.output(taggingsSelectOutput)
|
|
51
|
+
.mutation(async ({ ctx, input }) => {
|
|
52
|
+
return handleTRPCResult(await tagService.link(input, ctx));
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
unlink: procedure
|
|
56
|
+
.input(tagLinkSchema)
|
|
57
|
+
.output(tagsSelectOutput)
|
|
58
|
+
.mutation(async ({ ctx, input }) => {
|
|
59
|
+
return handleTRPCResult(await tagService.unlink(input, ctx));
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies pagination (limit and offset) to a drizzle query builder.
|
|
3
|
+
* Returns the query builder with pagination applied, or the original query if no limit is specified.
|
|
4
|
+
* Page is 1-based and only applied if limit is also provided.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const applyPagination = <TQuery>(query: TQuery, limit?: number, page?: number): TQuery => {
|
|
8
|
+
if (limit) (query as any).limit(limit);
|
|
9
|
+
|
|
10
|
+
if (page && page > 1 && limit) (query as any).offset((page - 1) * limit);
|
|
11
|
+
|
|
12
|
+
return query;
|
|
13
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { asc, desc } from "drizzle-orm";
|
|
2
|
+
import type { SQLiteTableWithColumns } from "drizzle-orm/sqlite-core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies sorting to a drizzle query builder.
|
|
6
|
+
* Returns the query builder with sorting applied.
|
|
7
|
+
* If no sort or order is specified, defaults to createdAt descending.
|
|
8
|
+
* If createdAt column doesn't exist, returns the query unchanged.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const applySorting = <TQuery, TTable extends SQLiteTableWithColumns<any>>(
|
|
12
|
+
query: TQuery,
|
|
13
|
+
table: TTable,
|
|
14
|
+
sort?: string,
|
|
15
|
+
order?: "asc" | "desc"
|
|
16
|
+
): TQuery => {
|
|
17
|
+
const column = sort ? table[sort] : table.createdAt || table.id;
|
|
18
|
+
if (!column) throw new Error(`Column ${sort} not found in table ${table.name}`);
|
|
19
|
+
(query as any).orderBy(order === "asc" ? asc(column) : desc(column));
|
|
20
|
+
return query;
|
|
21
|
+
};
|