@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,79 @@
|
|
|
1
|
+
import { integer, sqliteTable as table, 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 recurrence = table("recurrence", {
|
|
6
|
+
id: text("id").primaryKey().$default(uuidv4),
|
|
7
|
+
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
8
|
+
organizationId: text("organization_id").references(() => organizations.id, {
|
|
9
|
+
onDelete: "cascade",
|
|
10
|
+
}),
|
|
11
|
+
teamId: text("team_id").references(() => teams.id, { onDelete: "cascade" }),
|
|
12
|
+
name: text("name"),
|
|
13
|
+
kind: text("kind"),
|
|
14
|
+
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
15
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
16
|
+
.notNull()
|
|
17
|
+
.$default(() => new Date()),
|
|
18
|
+
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
19
|
+
.notNull()
|
|
20
|
+
.$default(() => new Date()),
|
|
21
|
+
metadata: text("metadata", { mode: "json" }).$type<Record<string, any>>(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const recurrenceRules = table("recurrence_rules", {
|
|
25
|
+
id: text("id").primaryKey().$default(uuidv4),
|
|
26
|
+
createdAt: integer("created_at", { mode: "timestamp" })
|
|
27
|
+
.notNull()
|
|
28
|
+
.$default(() => new Date()),
|
|
29
|
+
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
30
|
+
.notNull()
|
|
31
|
+
.$default(() => new Date()),
|
|
32
|
+
recurrenceId: text("recurrence_id").references(() => recurrence.id, {
|
|
33
|
+
onDelete: "cascade",
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
// Required: frequency
|
|
37
|
+
freq: integer("freq").notNull(),
|
|
38
|
+
|
|
39
|
+
// Start date; base for recurrence and source for missing instance params
|
|
40
|
+
dtstart: integer("dtstart", { mode: "timestamp" }),
|
|
41
|
+
|
|
42
|
+
// Interval between each freq iteration (default 1)
|
|
43
|
+
interval: integer("interval").notNull().default(1),
|
|
44
|
+
|
|
45
|
+
// Week start: MO, TU, WE, ... or integer 0–6
|
|
46
|
+
wkst: integer("wkst"),
|
|
47
|
+
|
|
48
|
+
// How many occurrences to generate
|
|
49
|
+
count: integer("count"),
|
|
50
|
+
|
|
51
|
+
// Last occurrence date (inclusive)
|
|
52
|
+
until: integer("until", { mode: "timestamp" }),
|
|
53
|
+
|
|
54
|
+
// IANA timezone string (Intl API)
|
|
55
|
+
tzid: text("tzid"),
|
|
56
|
+
|
|
57
|
+
// BYSETPOS: occurrence number(s) in the frequency period (e.g. -1 = last)
|
|
58
|
+
bysetpos: text("bysetpos", { mode: "json" }).$type<number | number[]>(),
|
|
59
|
+
|
|
60
|
+
// BYMONTH: month(s) 1–12
|
|
61
|
+
bymonth: text("bymonth", { mode: "json" }).$type<number | number[]>(),
|
|
62
|
+
|
|
63
|
+
// BYMONTHDAY: day(s) of month
|
|
64
|
+
bymonthday: text("bymonthday", { mode: "json" }).$type<number | number[]>(),
|
|
65
|
+
|
|
66
|
+
// BYYEARDAY: day(s) of year
|
|
67
|
+
byyearday: text("byyearday", { mode: "json" }).$type<number | number[]>(),
|
|
68
|
+
|
|
69
|
+
// BYWEEKNO: week number(s) (ISO8601)
|
|
70
|
+
byweekno: text("byweekno", { mode: "json" }).$type<number | number[]>(),
|
|
71
|
+
|
|
72
|
+
// BYWEEKDAY: weekday(s) 0–6, or nth e.g. { weekday: 4, n: 1 } for first Friday
|
|
73
|
+
byweekday: text("byweekday", { mode: "json" }).$type<number | number[]>(),
|
|
74
|
+
|
|
75
|
+
// BYHOUR, BYMINUTE, BYSECOND
|
|
76
|
+
byhour: text("byhour", { mode: "json" }).$type<number | number[]>(),
|
|
77
|
+
byminute: text("byminute", { mode: "json" }).$type<number | number[]>(),
|
|
78
|
+
bysecond: text("bysecond", { mode: "json" }).$type<number | number[]>(),
|
|
79
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
|
|
2
|
+
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
3
|
+
import { ok } from "neverthrow";
|
|
4
|
+
import type { ServerResultAsync } from "#modules/base/base.dto";
|
|
5
|
+
import { BaseTableRepository } from "#modules/base/base.repository";
|
|
6
|
+
import * as recurrence from "#modules/recurrence/recurrence.db";
|
|
7
|
+
|
|
8
|
+
const schema = { ...recurrence };
|
|
9
|
+
type Schema = typeof schema;
|
|
10
|
+
type Orm = LibSQLDatabase<Schema>;
|
|
11
|
+
type RecurrenceInsert = InferInsertModel<Schema["recurrence"]>;
|
|
12
|
+
type RecurrenceRulesInsert = InferInsertModel<Schema["recurrenceRules"]>;
|
|
13
|
+
|
|
14
|
+
/** Rule input for create: rule fields without id, recurrenceId, createdAt, updatedAt */
|
|
15
|
+
export type CreateRecurrenceRuleInput = Omit<
|
|
16
|
+
RecurrenceRulesInsert,
|
|
17
|
+
"id" | "recurrenceId" | "createdAt" | "updatedAt"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export interface CreateWithRulesResult {
|
|
21
|
+
recurrence: InferSelectModel<Schema["recurrence"]>;
|
|
22
|
+
rules: InferSelectModel<Schema["recurrenceRules"]>[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class RecurrenceRepository extends BaseTableRepository<
|
|
26
|
+
Orm,
|
|
27
|
+
Schema,
|
|
28
|
+
Record<string, never>,
|
|
29
|
+
Schema["recurrence"]
|
|
30
|
+
> {
|
|
31
|
+
async createWithRules(
|
|
32
|
+
recurrenceData: RecurrenceInsert,
|
|
33
|
+
rulesData: CreateRecurrenceRuleInput[],
|
|
34
|
+
tx?: Orm
|
|
35
|
+
): ServerResultAsync<CreateWithRulesResult> {
|
|
36
|
+
return this.throwableAsync(async () => {
|
|
37
|
+
const db = tx ?? this.orm;
|
|
38
|
+
const result = await db.transaction(async (trx) => {
|
|
39
|
+
const [createdRecurrence] = await trx
|
|
40
|
+
.insert(this.table)
|
|
41
|
+
.values(recurrenceData)
|
|
42
|
+
.returning();
|
|
43
|
+
if (!createdRecurrence) throw new Error("Failed to create recurrence");
|
|
44
|
+
|
|
45
|
+
const rulesWithRecurrenceId: RecurrenceRulesInsert[] = rulesData.map((rule) => ({
|
|
46
|
+
...rule,
|
|
47
|
+
recurrenceId: createdRecurrence.id,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const insertedRules =
|
|
51
|
+
rulesWithRecurrenceId.length > 0
|
|
52
|
+
? await trx
|
|
53
|
+
.insert(this.schema.recurrenceRules)
|
|
54
|
+
.values(rulesWithRecurrenceId)
|
|
55
|
+
.returning()
|
|
56
|
+
: [];
|
|
57
|
+
|
|
58
|
+
return ok({ recurrence: createdRecurrence, rules: insertedRules });
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class RecurrenceRulesRepository extends BaseTableRepository<
|
|
66
|
+
Orm,
|
|
67
|
+
Schema,
|
|
68
|
+
Record<string, never>,
|
|
69
|
+
Schema["recurrenceRules"]
|
|
70
|
+
> {}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateRecurrenceSchema,
|
|
3
|
+
DeleteRecurrenceRulesSchema,
|
|
4
|
+
DeleteRecurrenceSchema,
|
|
5
|
+
UpdateRecurrenceRulesSchema,
|
|
6
|
+
UpdateRecurrenceSchema,
|
|
7
|
+
} from "@m5kdev/commons/modules/recurrence/recurrence.schema";
|
|
8
|
+
import type { QueryInput } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
9
|
+
import { err, ok } from "neverthrow";
|
|
10
|
+
import type { Context, 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 {
|
|
14
|
+
CreateRecurrenceRuleInput,
|
|
15
|
+
CreateWithRulesResult,
|
|
16
|
+
RecurrenceRepository,
|
|
17
|
+
RecurrenceRulesRepository,
|
|
18
|
+
} from "#modules/recurrence/recurrence.repository";
|
|
19
|
+
|
|
20
|
+
const RECURRENCE_RULE_INSERT_KEYS = [
|
|
21
|
+
"freq",
|
|
22
|
+
"dtstart",
|
|
23
|
+
"interval",
|
|
24
|
+
"wkst",
|
|
25
|
+
"count",
|
|
26
|
+
"until",
|
|
27
|
+
"tzid",
|
|
28
|
+
"bysetpos",
|
|
29
|
+
"bymonth",
|
|
30
|
+
"bymonthday",
|
|
31
|
+
"byyearday",
|
|
32
|
+
"byweekno",
|
|
33
|
+
"byweekday",
|
|
34
|
+
"byhour",
|
|
35
|
+
"byminute",
|
|
36
|
+
"bysecond",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
function mapRuleToInsert(
|
|
40
|
+
rule: CreateRecurrenceSchema["recurrenceRules"][number]
|
|
41
|
+
): CreateRecurrenceRuleInput {
|
|
42
|
+
const out: Record<string, unknown> = {};
|
|
43
|
+
for (const key of RECURRENCE_RULE_INSERT_KEYS) {
|
|
44
|
+
if (key in rule && rule[key as keyof typeof rule] !== undefined) {
|
|
45
|
+
out[key] = rule[key as keyof typeof rule];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out as CreateRecurrenceRuleInput;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class RecurrenceService extends BaseService<
|
|
52
|
+
{ recurrence: RecurrenceRepository; recurrenceRules: RecurrenceRulesRepository },
|
|
53
|
+
Record<string, never>
|
|
54
|
+
> {
|
|
55
|
+
async create(
|
|
56
|
+
data: CreateRecurrenceSchema,
|
|
57
|
+
ctx: Context
|
|
58
|
+
): ServerResultAsync<CreateWithRulesResult> {
|
|
59
|
+
const recurrenceData = {
|
|
60
|
+
name: data.name,
|
|
61
|
+
kind: data.kind,
|
|
62
|
+
enabled: data.enabled,
|
|
63
|
+
metadata: data.metadata ?? null,
|
|
64
|
+
userId: ctx.user?.id ?? null,
|
|
65
|
+
organizationId: ctx.session.activeOrganizationId ?? null,
|
|
66
|
+
teamId: ctx.session.activeTeamId ?? null,
|
|
67
|
+
};
|
|
68
|
+
const rulesData = data.recurrenceRules.map(mapRuleToInsert);
|
|
69
|
+
return this.repository.recurrence.createWithRules(recurrenceData, rulesData);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async list(
|
|
73
|
+
query?: QueryInput,
|
|
74
|
+
ctx?: { user?: User }
|
|
75
|
+
): ServerResultAsync<{ rows: CreateWithRulesResult["recurrence"][]; total: number }> {
|
|
76
|
+
const queryWithUser = ctx?.user ? this.addUserFilter(ctx.user.id, query, "userId") : query;
|
|
77
|
+
return this.repository.recurrence.queryList(queryWithUser);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async findById(id: string): ServerResultAsync<CreateWithRulesResult["recurrence"] | null> {
|
|
81
|
+
const result = await this.repository.recurrence.findById(id);
|
|
82
|
+
if (result.isErr()) return err(result.error);
|
|
83
|
+
return ok(result.value ?? null);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async update(
|
|
87
|
+
data: UpdateRecurrenceSchema & { id: string }
|
|
88
|
+
): ServerResultAsync<CreateWithRulesResult["recurrence"]> {
|
|
89
|
+
return this.repository.recurrence.update(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async updateRule(
|
|
93
|
+
data: UpdateRecurrenceRulesSchema
|
|
94
|
+
): ServerResultAsync<CreateWithRulesResult["rules"][number]> {
|
|
95
|
+
return this.repository.recurrenceRules.update(data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async delete(data: DeleteRecurrenceSchema): ServerResultAsync<{ id: string }> {
|
|
99
|
+
return this.repository.recurrence.deleteById(data.id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async deleteRule(data: DeleteRecurrenceRulesSchema): ServerResultAsync<{ id: string }> {
|
|
103
|
+
return this.repository.recurrenceRules.deleteById(data.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRecurrenceSchema,
|
|
3
|
+
deleteRecurrenceRulesSchema,
|
|
4
|
+
deleteRecurrenceSchema,
|
|
5
|
+
recurrenceRulesSchema,
|
|
6
|
+
recurrenceSchema,
|
|
7
|
+
updateRecurrenceRulesSchema,
|
|
8
|
+
updateRecurrenceSchema,
|
|
9
|
+
} from "@m5kdev/commons/modules/recurrence/recurrence.schema";
|
|
10
|
+
import { querySchema } from "@m5kdev/commons/modules/schemas/query.schema";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { RecurrenceService } from "#modules/recurrence/recurrence.service";
|
|
13
|
+
import { handleTRPCResult, procedure, router } from "#trpc";
|
|
14
|
+
|
|
15
|
+
const createRecurrenceOutputSchema = z.object({
|
|
16
|
+
recurrence: recurrenceSchema,
|
|
17
|
+
rules: z.array(recurrenceRulesSchema),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const listRecurrenceOutputSchema = z.object({
|
|
21
|
+
rows: z.array(recurrenceSchema),
|
|
22
|
+
total: z.number(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const updateRecurrenceInputSchema = updateRecurrenceSchema.extend({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const deleteRecurrenceOutputSchema = z.object({ id: z.string() });
|
|
30
|
+
|
|
31
|
+
export function createRecurrenceTRPC(recurrenceService: RecurrenceService) {
|
|
32
|
+
return router({
|
|
33
|
+
list: procedure
|
|
34
|
+
.input(querySchema.optional())
|
|
35
|
+
.output(listRecurrenceOutputSchema)
|
|
36
|
+
.query(async ({ ctx, input }) => {
|
|
37
|
+
return handleTRPCResult(await recurrenceService.list(input, ctx));
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
create: procedure
|
|
41
|
+
.input(createRecurrenceSchema)
|
|
42
|
+
.output(createRecurrenceOutputSchema)
|
|
43
|
+
.mutation(async ({ ctx, input }) => {
|
|
44
|
+
return handleTRPCResult(await recurrenceService.create(input, ctx));
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
findById: procedure
|
|
48
|
+
.input(z.object({ id: z.string() }))
|
|
49
|
+
.output(recurrenceSchema.nullable())
|
|
50
|
+
.query(async ({ input }) => {
|
|
51
|
+
return handleTRPCResult(await recurrenceService.findById(input.id));
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
update: procedure
|
|
55
|
+
.input(updateRecurrenceInputSchema)
|
|
56
|
+
.output(recurrenceSchema)
|
|
57
|
+
.mutation(async ({ input }) => {
|
|
58
|
+
return handleTRPCResult(await recurrenceService.update(input));
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
updateRule: procedure
|
|
62
|
+
.input(updateRecurrenceRulesSchema)
|
|
63
|
+
.output(recurrenceRulesSchema)
|
|
64
|
+
.mutation(async ({ input }) => {
|
|
65
|
+
return handleTRPCResult(await recurrenceService.updateRule(input));
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
delete: procedure
|
|
69
|
+
.input(deleteRecurrenceSchema)
|
|
70
|
+
.output(deleteRecurrenceOutputSchema)
|
|
71
|
+
.mutation(async ({ input }) => {
|
|
72
|
+
return handleTRPCResult(await recurrenceService.delete(input));
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
deleteRule: procedure
|
|
76
|
+
.input(deleteRecurrenceRulesSchema)
|
|
77
|
+
.output(deleteRecurrenceOutputSchema)
|
|
78
|
+
.mutation(async ({ input }) => {
|
|
79
|
+
return handleTRPCResult(await recurrenceService.deleteRule(input));
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const socialMediaInputSchema = z.object({
|
|
4
|
+
s3Path: z.string().min(1, "Media S3 path is required"),
|
|
5
|
+
mediaType: z.enum(["image", "video", "document"]).optional(),
|
|
6
|
+
title: z.string().max(300).optional(),
|
|
7
|
+
description: z.string().max(2000).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const socialPostInputSchema = z.object({
|
|
11
|
+
text: z.string().min(1, "Post text is required"),
|
|
12
|
+
media: z.array(socialMediaInputSchema).max(4).optional(),
|
|
13
|
+
visibility: z.enum(["PUBLIC", "CONNECTIONS"]).default("PUBLIC"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const socialPostOutputSchema = z.object({
|
|
17
|
+
shareUrn: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type SocialPostInput = z.infer<typeof socialPostInputSchema>;
|
|
21
|
+
export type SocialMediaInput = z.infer<typeof socialMediaInputSchema>;
|
|
22
|
+
export type SocialPostOutput = z.infer<typeof socialPostOutputSchema>;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { escapeLinkedInText } from "./social.linkedin";
|
|
2
|
+
|
|
3
|
+
describe("escapeLinkedInText", () => {
|
|
4
|
+
describe("special character escaping", () => {
|
|
5
|
+
it("escapes backslash", () => {
|
|
6
|
+
expect(escapeLinkedInText("path\\to\\file")).toBe("path\\\\to\\\\file");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("escapes pipe", () => {
|
|
10
|
+
expect(escapeLinkedInText("a|b")).toBe("a\\|b");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("escapes curly braces", () => {
|
|
14
|
+
expect(escapeLinkedInText("{value}")).toBe("\\{value\\}");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("escapes at symbol", () => {
|
|
18
|
+
expect(escapeLinkedInText("email@example.com")).toBe("email\\@example.com");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("escapes square brackets", () => {
|
|
22
|
+
expect(escapeLinkedInText("[item]")).toBe("\\[item\\]");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("escapes parentheses", () => {
|
|
26
|
+
expect(escapeLinkedInText("(note)")).toBe("\\(note\\)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("escapes angle brackets", () => {
|
|
30
|
+
expect(escapeLinkedInText("<html>")).toBe("\\<html\\>");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("escapes hash when not a hashtag", () => {
|
|
34
|
+
expect(escapeLinkedInText("item #1")).toBe("item \\#1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("escapes asterisk", () => {
|
|
38
|
+
expect(escapeLinkedInText("*bold*")).toBe("\\*bold\\*");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("escapes underscore", () => {
|
|
42
|
+
expect(escapeLinkedInText("_italic_")).toBe("\\_italic\\_");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("escapes tilde", () => {
|
|
46
|
+
expect(escapeLinkedInText("~strikethrough~")).toBe("\\~strikethrough\\~");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("escapes multiple special characters together", () => {
|
|
50
|
+
expect(escapeLinkedInText("Check out (this) & [that]!")).toBe(
|
|
51
|
+
"Check out \\(this\\) & \\[that\\]!"
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("mentions preservation", () => {
|
|
57
|
+
it("preserves person mention", () => {
|
|
58
|
+
const input = "Hello @[John Doe](urn:li:person:123456)!";
|
|
59
|
+
const result = escapeLinkedInText(input);
|
|
60
|
+
expect(result).toBe("Hello @[John Doe](urn:li:person:123456)!");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves organization mention", () => {
|
|
64
|
+
const input = "Check out @[DevtestCo](urn:li:organization:2414183)";
|
|
65
|
+
const result = escapeLinkedInText(input);
|
|
66
|
+
expect(result).toBe("Check out @[DevtestCo](urn:li:organization:2414183)");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("preserves mention without fallback text", () => {
|
|
70
|
+
const input = "Mention @[](urn:li:person:123) here";
|
|
71
|
+
const result = escapeLinkedInText(input);
|
|
72
|
+
expect(result).toBe("Mention @[](urn:li:person:123) here");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("preserves multiple mentions", () => {
|
|
76
|
+
const input = "Thanks @[Alice](urn:li:person:111) and @[Bob](urn:li:person:222)!";
|
|
77
|
+
const result = escapeLinkedInText(input);
|
|
78
|
+
expect(result).toBe("Thanks @[Alice](urn:li:person:111) and @[Bob](urn:li:person:222)!");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("escapes text around mentions", () => {
|
|
82
|
+
const input = "(Hello) @[John](urn:li:person:123) [world]";
|
|
83
|
+
const result = escapeLinkedInText(input);
|
|
84
|
+
expect(result).toBe("\\(Hello\\) @[John](urn:li:person:123) \\[world\\]");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("hashtag template preservation", () => {
|
|
89
|
+
it("preserves hashtag template with escaped hash", () => {
|
|
90
|
+
const input = "Check {hashtag|\\#|MyTag}";
|
|
91
|
+
const result = escapeLinkedInText(input);
|
|
92
|
+
expect(result).toBe("Check {hashtag|\\#|MyTag}");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("preserves hashtag template with unescaped hash", () => {
|
|
96
|
+
const input = "Check {hashtag|#|MyTag}";
|
|
97
|
+
const result = escapeLinkedInText(input);
|
|
98
|
+
expect(result).toBe("Check {hashtag|#|MyTag}");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("preserves hashtag template with fullwidth hash", () => {
|
|
102
|
+
const input = "Check {hashtag|#|MyTag}";
|
|
103
|
+
const result = escapeLinkedInText(input);
|
|
104
|
+
expect(result).toBe("Check {hashtag|#|MyTag}");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("preserves multiple hashtag templates", () => {
|
|
108
|
+
const input = "{hashtag|\\#|Tag1} and {hashtag|\\#|Tag2}";
|
|
109
|
+
const result = escapeLinkedInText(input);
|
|
110
|
+
expect(result).toBe("{hashtag|\\#|Tag1} and {hashtag|\\#|Tag2}");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("simple hashtag conversion", () => {
|
|
115
|
+
it("converts simple hashtag to template format", () => {
|
|
116
|
+
const input = "Check out #MyHashtag";
|
|
117
|
+
const result = escapeLinkedInText(input);
|
|
118
|
+
expect(result).toBe("Check out {hashtag|\\#|MyHashtag}");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("converts multiple simple hashtags", () => {
|
|
122
|
+
const input = "#Hello #World";
|
|
123
|
+
const result = escapeLinkedInText(input);
|
|
124
|
+
expect(result).toBe("{hashtag|\\#|Hello} {hashtag|\\#|World}");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("converts hashtag with accented characters", () => {
|
|
128
|
+
const input = "#Café #Naïve";
|
|
129
|
+
const result = escapeLinkedInText(input);
|
|
130
|
+
expect(result).toBe("{hashtag|\\#|Café} {hashtag|\\#|Naïve}");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("handles hashtag at end of text", () => {
|
|
134
|
+
const input = "Great post #Amazing";
|
|
135
|
+
const result = escapeLinkedInText(input);
|
|
136
|
+
expect(result).toBe("Great post {hashtag|\\#|Amazing}");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does not convert hash followed by number only", () => {
|
|
140
|
+
const input = "Item #1 is great";
|
|
141
|
+
const result = escapeLinkedInText(input);
|
|
142
|
+
expect(result).toBe("Item \\#1 is great");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("mixed content", () => {
|
|
147
|
+
it("handles mentions, hashtags, and special chars together", () => {
|
|
148
|
+
const input =
|
|
149
|
+
"Hello @[John](urn:li:person:123)! Check out #Tech & (amazing) *stuff*";
|
|
150
|
+
const result = escapeLinkedInText(input);
|
|
151
|
+
expect(result).toBe(
|
|
152
|
+
"Hello @[John](urn:li:person:123)! Check out {hashtag|\\#|Tech} & \\(amazing\\) \\*stuff\\*"
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("handles pre-formatted hashtag template with mentions", () => {
|
|
157
|
+
const input =
|
|
158
|
+
"@[Company](urn:li:organization:123) is doing {hashtag|\\#|GreatThings}!";
|
|
159
|
+
const result = escapeLinkedInText(input);
|
|
160
|
+
expect(result).toBe(
|
|
161
|
+
"@[Company](urn:li:organization:123) is doing {hashtag|\\#|GreatThings}!"
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("preserves newlines and escapes special chars", () => {
|
|
166
|
+
const input = "Line 1 (note)\nLine 2 [item]";
|
|
167
|
+
const result = escapeLinkedInText(input);
|
|
168
|
+
expect(result).toBe("Line 1 \\(note\\)\nLine 2 \\[item\\]");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("edge cases", () => {
|
|
173
|
+
it("handles empty string", () => {
|
|
174
|
+
expect(escapeLinkedInText("")).toBe("");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles plain text without special characters", () => {
|
|
178
|
+
const input = "Hello world this is plain text";
|
|
179
|
+
expect(escapeLinkedInText(input)).toBe("Hello world this is plain text");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles text with only special characters", () => {
|
|
183
|
+
expect(escapeLinkedInText("@#$")).toBe("\\@\\#$");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not double-escape already escaped backslashes in mentions", () => {
|
|
187
|
+
const input = "@[Test\\Name](urn:li:person:123)";
|
|
188
|
+
const result = escapeLinkedInText(input);
|
|
189
|
+
// The mention is preserved as-is, including internal backslash
|
|
190
|
+
expect(result).toBe("@[Test\\Name](urn:li:person:123)");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("real-world posts", () => {
|
|
195
|
+
it("handles a full LinkedIn post with hashtags and special characters", () => {
|
|
196
|
+
const input = `I tried that prompt going around lately: "Draw a picture of how I'm treating you."
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
My result was just a happy little bot (picture included).
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
But when I scrolled through LinkedIn, I noticed something different. People were posting unhappy bots: overworked assistants and robots in chains. There was a lot of "you did it wrong" or "fix it" energy.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
Here is the thing: I don't anthropomorphize AI. I don't chat with it about my day. I use it as a tool for specific tasks. Yet, I still find myself typing "please" and "thank you" in my prompts.
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
I am the same way with games. In RPGs, I almost always pick the "good guy" route. I tried to play the renegade path in Mass Effect once and couldn't even finish it. It just felt wrong.
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
Contrast that with a trend I've been seeing lately. There are papers and plenty of anecdotes suggesting that negative feedback, harsh wording, or even subtle "threats" can actually squeeze better answers out of these models.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
That creates a weird tension for me:
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
Intellectually, I know adversarial prompting can work.
|
|
218
|
+
Practically, I can't bring myself to be mean to what is essentially linear algebra wrapped in a UI.
|
|
219
|
+
Personally, I don't think training myself to be rude, even to a tool, is a habit I want to cultivate.
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
Maybe the real optimization isn't about being harsh, but being clear. Well-scoped tasks, concrete constraints, and explicit feedback usually do the heavy lifting anyway.
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
I'm curious how others handle this, especially if you spend most of day in AI tools:
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
Do you consciously change your tone to get better results?
|
|
229
|
+
Have you actually seen consistent gains from theating or adversarial prompts?
|
|
230
|
+
Do you think how we talk to AI will eventually bleed into how we talk to humans?
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
#AI #LLM #PromptEngineering`;
|
|
234
|
+
|
|
235
|
+
const expected = `I tried that prompt going around lately: "Draw a picture of how I'm treating you."
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
My result was just a happy little bot \\(picture included\\).
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
But when I scrolled through LinkedIn, I noticed something different. People were posting unhappy bots: overworked assistants and robots in chains. There was a lot of "you did it wrong" or "fix it" energy.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
Here is the thing: I don't anthropomorphize AI. I don't chat with it about my day. I use it as a tool for specific tasks. Yet, I still find myself typing "please" and "thank you" in my prompts.
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
I am the same way with games. In RPGs, I almost always pick the "good guy" route. I tried to play the renegade path in Mass Effect once and couldn't even finish it. It just felt wrong.
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
Contrast that with a trend I've been seeing lately. There are papers and plenty of anecdotes suggesting that negative feedback, harsh wording, or even subtle "threats" can actually squeeze better answers out of these models.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
That creates a weird tension for me:
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
Intellectually, I know adversarial prompting can work.
|
|
257
|
+
Practically, I can't bring myself to be mean to what is essentially linear algebra wrapped in a UI.
|
|
258
|
+
Personally, I don't think training myself to be rude, even to a tool, is a habit I want to cultivate.
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
Maybe the real optimization isn't about being harsh, but being clear. Well-scoped tasks, concrete constraints, and explicit feedback usually do the heavy lifting anyway.
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
I'm curious how others handle this, especially if you spend most of day in AI tools:
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Do you consciously change your tone to get better results?
|
|
268
|
+
Have you actually seen consistent gains from theating or adversarial prompts?
|
|
269
|
+
Do you think how we talk to AI will eventually bleed into how we talk to humans?
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
{hashtag|\\#|AI} {hashtag|\\#|LLM} {hashtag|\\#|PromptEngineering}`;
|
|
273
|
+
|
|
274
|
+
expect(escapeLinkedInText(input)).toBe(expected);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|