@m5kdev/backend 0.1.1 → 0.1.2
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/src/lib/posthog.js +7 -0
- package/dist/src/lib/sentry.js +9 -0
- package/dist/src/modules/access/access.repository.js +32 -0
- package/dist/src/modules/access/access.service.js +51 -0
- package/dist/src/modules/access/access.test.js +182 -0
- package/dist/src/modules/access/access.utils.js +20 -0
- package/dist/src/modules/ai/ai.db.js +39 -0
- package/dist/src/modules/ai/ai.prompt.js +30 -0
- package/dist/src/modules/ai/ai.repository.js +26 -0
- package/dist/src/modules/ai/ai.router.js +132 -0
- package/dist/src/modules/ai/ai.service.js +207 -0
- package/dist/src/modules/ai/ai.trpc.d.ts +5 -5
- package/dist/src/modules/ai/ai.trpc.js +20 -0
- package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
- package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
- package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
- package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
- package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
- package/dist/src/modules/auth/auth.db.js +215 -0
- package/dist/src/modules/auth/auth.dto.js +38 -0
- package/dist/src/modules/auth/auth.lib.d.ts +4 -4
- package/dist/src/modules/auth/auth.lib.js +284 -0
- package/dist/src/modules/auth/auth.middleware.js +52 -0
- package/dist/src/modules/auth/auth.repository.js +541 -0
- package/dist/src/modules/auth/auth.service.js +201 -0
- package/dist/src/modules/auth/auth.trpc.d.ts +18 -18
- package/dist/src/modules/auth/auth.trpc.js +157 -0
- package/dist/src/modules/auth/auth.utils.js +97 -0
- package/dist/src/modules/base/base.abstract.js +53 -0
- package/dist/src/modules/base/base.dto.js +112 -0
- package/dist/src/modules/base/base.grants.js +123 -0
- package/dist/src/modules/base/base.grants.test.js +668 -0
- package/dist/src/modules/base/base.repository.js +307 -0
- package/dist/src/modules/base/base.service.js +109 -0
- package/dist/src/modules/base/base.types.js +2 -0
- package/dist/src/modules/billing/billing.db.js +29 -0
- package/dist/src/modules/billing/billing.repository.js +235 -0
- package/dist/src/modules/billing/billing.router.js +56 -0
- package/dist/src/modules/billing/billing.service.js +147 -0
- package/dist/src/modules/billing/billing.trpc.d.ts +5 -5
- package/dist/src/modules/billing/billing.trpc.js +17 -0
- package/dist/src/modules/clay/clay.repository.js +26 -0
- package/dist/src/modules/clay/clay.service.js +24 -0
- package/dist/src/modules/connect/connect.db.js +30 -0
- package/dist/src/modules/connect/connect.dto.js +36 -0
- package/dist/src/modules/connect/connect.linkedin.js +53 -0
- package/dist/src/modules/connect/connect.oauth.js +198 -0
- package/dist/src/modules/connect/connect.repository.d.ts +7 -7
- package/dist/src/modules/connect/connect.repository.js +54 -0
- package/dist/src/modules/connect/connect.router.js +54 -0
- package/dist/src/modules/connect/connect.service.d.ts +14 -14
- package/dist/src/modules/connect/connect.service.js +114 -0
- package/dist/src/modules/connect/connect.trpc.d.ts +10 -10
- package/dist/src/modules/connect/connect.trpc.js +21 -0
- package/dist/src/modules/connect/connect.types.js +2 -0
- package/dist/src/modules/crypto/crypto.db.js +17 -0
- package/dist/src/modules/crypto/crypto.repository.js +10 -0
- package/dist/src/modules/crypto/crypto.service.js +52 -0
- package/dist/src/modules/email/email.service.js +107 -0
- package/dist/src/modules/file/file.repository.js +79 -0
- package/dist/src/modules/file/file.router.js +99 -0
- package/dist/src/modules/file/file.service.js +150 -0
- package/dist/src/modules/recurrence/recurrence.db.js +66 -0
- package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
- package/dist/src/modules/recurrence/recurrence.service.js +70 -0
- package/dist/src/modules/recurrence/recurrence.trpc.d.ts +15 -15
- package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
- package/dist/src/modules/social/social.dto.js +18 -0
- package/dist/src/modules/social/social.linkedin.js +427 -0
- package/dist/src/modules/social/social.linkedin.test.js +235 -0
- package/dist/src/modules/social/social.service.js +76 -0
- package/dist/src/modules/social/social.types.js +2 -0
- package/dist/src/modules/tag/tag.db.js +42 -0
- package/dist/src/modules/tag/tag.dto.js +9 -0
- package/dist/src/modules/tag/tag.repository.js +154 -0
- package/dist/src/modules/tag/tag.service.js +31 -0
- package/dist/src/modules/tag/tag.trpc.d.ts +5 -5
- package/dist/src/modules/tag/tag.trpc.js +47 -0
- package/dist/src/modules/utils/applyPagination.js +16 -0
- package/dist/src/modules/utils/applySorting.js +18 -0
- package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
- package/dist/src/modules/video/video.service.js +84 -0
- package/dist/src/modules/webhook/webhook.constants.js +10 -0
- package/dist/src/modules/webhook/webhook.db.js +17 -0
- package/dist/src/modules/webhook/webhook.dto.js +7 -0
- package/dist/src/modules/webhook/webhook.repository.js +56 -0
- package/dist/src/modules/webhook/webhook.router.js +30 -0
- package/dist/src/modules/webhook/webhook.service.js +68 -0
- package/dist/src/modules/workflow/workflow.db.js +30 -0
- package/dist/src/modules/workflow/workflow.repository.js +105 -0
- package/dist/src/modules/workflow/workflow.service.js +37 -0
- package/dist/src/modules/workflow/workflow.trpc.d.ts +5 -5
- package/dist/src/modules/workflow/workflow.trpc.js +21 -0
- package/dist/src/modules/workflow/workflow.types.js +2 -0
- package/dist/src/modules/workflow/workflow.utils.js +173 -0
- package/dist/src/test/stubs/utils.js +5 -0
- package/dist/src/trpc/context.d.ts +5 -5
- package/dist/src/trpc/context.js +17 -0
- package/dist/src/trpc/index.js +6 -0
- package/dist/src/trpc/procedures.d.ts +56 -56
- package/dist/src/trpc/procedures.js +32 -0
- package/dist/src/trpc/utils.js +20 -0
- package/dist/src/types.d.ts +33 -33
- package/dist/src/types.js +13 -0
- package/dist/src/utils/errors.js +104 -0
- package/dist/src/utils/logger.js +11 -0
- package/dist/src/utils/posthog.js +31 -0
- package/dist/src/utils/types.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseExternaRepository = exports.BaseTableRepository = exports.BaseRepository = exports.arrayContains = exports.TableConditionBuilder = exports.ConditionBuilder = void 0;
|
|
4
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
5
|
+
const neverthrow_1 = require("neverthrow");
|
|
6
|
+
const base_abstract_1 = require("#modules/base/base.abstract");
|
|
7
|
+
const base_dto_1 = require("#modules/base/base.dto");
|
|
8
|
+
const applyPagination_1 = require("#modules/utils/applyPagination");
|
|
9
|
+
const applySorting_1 = require("#modules/utils/applySorting");
|
|
10
|
+
const getConditionsFromFilters_1 = require("#modules/utils/getConditionsFromFilters");
|
|
11
|
+
class ConditionBuilder {
|
|
12
|
+
conditions;
|
|
13
|
+
constructor(conditions = []) {
|
|
14
|
+
this.conditions = conditions;
|
|
15
|
+
this.conditions = conditions;
|
|
16
|
+
}
|
|
17
|
+
push(condition) {
|
|
18
|
+
if (condition)
|
|
19
|
+
this.conditions.push(condition);
|
|
20
|
+
}
|
|
21
|
+
join(type = "and") {
|
|
22
|
+
if (this.conditions.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (this.conditions.length === 1)
|
|
25
|
+
return this.conditions[0];
|
|
26
|
+
return type === "and" ? (0, drizzle_orm_1.and)(...this.conditions) : (0, drizzle_orm_1.or)(...this.conditions);
|
|
27
|
+
}
|
|
28
|
+
[Symbol.iterator]() {
|
|
29
|
+
return this.conditions[Symbol.iterator]();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.ConditionBuilder = ConditionBuilder;
|
|
33
|
+
class TableConditionBuilder extends ConditionBuilder {
|
|
34
|
+
table;
|
|
35
|
+
constructor(table) {
|
|
36
|
+
super();
|
|
37
|
+
this.table = table;
|
|
38
|
+
}
|
|
39
|
+
applyFilters({ filters } = {}) {
|
|
40
|
+
if (filters && filters.length > 0)
|
|
41
|
+
(0, getConditionsFromFilters_1.getConditionsFromFilters)(this, filters, this.table);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.TableConditionBuilder = TableConditionBuilder;
|
|
45
|
+
const arrayContains = (table, values) => {
|
|
46
|
+
const arrayContains = [];
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
arrayContains.push((0, drizzle_orm_1.like)(table, `%"${value}%"`));
|
|
49
|
+
}
|
|
50
|
+
return (0, drizzle_orm_1.or)(...arrayContains);
|
|
51
|
+
};
|
|
52
|
+
exports.arrayContains = arrayContains;
|
|
53
|
+
class BaseRepository extends base_abstract_1.Base {
|
|
54
|
+
orm;
|
|
55
|
+
schema;
|
|
56
|
+
repository;
|
|
57
|
+
constructor(options, repository) {
|
|
58
|
+
super("repository");
|
|
59
|
+
this.orm = options.orm;
|
|
60
|
+
this.schema = options.schema;
|
|
61
|
+
this.repository = repository;
|
|
62
|
+
}
|
|
63
|
+
getConditionBuilder(table) {
|
|
64
|
+
if (table === undefined) {
|
|
65
|
+
return new ConditionBuilder();
|
|
66
|
+
}
|
|
67
|
+
return new TableConditionBuilder(table);
|
|
68
|
+
}
|
|
69
|
+
withPagination(query, { page, limit }) {
|
|
70
|
+
return (0, applyPagination_1.applyPagination)(query, limit, page);
|
|
71
|
+
}
|
|
72
|
+
withSorting(query, { sort, order }, table) {
|
|
73
|
+
if (!table)
|
|
74
|
+
throw new Error("No table provided");
|
|
75
|
+
return (0, applySorting_1.applySorting)(query, table, sort, order);
|
|
76
|
+
}
|
|
77
|
+
withSortingAndPagination(query, { sort, order, page, limit }, table) {
|
|
78
|
+
if (!table)
|
|
79
|
+
throw new Error("No table provided");
|
|
80
|
+
return this.withSorting(this.withPagination(query, { page, limit }), { sort, order }, table);
|
|
81
|
+
}
|
|
82
|
+
addUserIdFilter(userId, query) {
|
|
83
|
+
const userIdFilter = {
|
|
84
|
+
columnId: "userId",
|
|
85
|
+
type: "string",
|
|
86
|
+
method: "equals",
|
|
87
|
+
value: userId,
|
|
88
|
+
};
|
|
89
|
+
return query
|
|
90
|
+
? { ...query, filters: [...(query?.filters ?? []), userIdFilter] }
|
|
91
|
+
: { filters: [userIdFilter] };
|
|
92
|
+
}
|
|
93
|
+
helpers = {
|
|
94
|
+
pickColumns: base_dto_1.pickColumns,
|
|
95
|
+
arrayContains: exports.arrayContains,
|
|
96
|
+
ConditionBuilder,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
exports.BaseRepository = BaseRepository;
|
|
100
|
+
/**
|
|
101
|
+
* Generic table-bound repository with typed CRUD, returning ServerResultAsync via throwableAsync.
|
|
102
|
+
*
|
|
103
|
+
* Example:
|
|
104
|
+
* const userRepo = new UserRepository(db, schema);
|
|
105
|
+
* class UserRepository extends BaseTableRepository<typeof schema.user> {
|
|
106
|
+
* constructor(db: LibSQLDatabase<typeof schema>, schema: typeof schema) {
|
|
107
|
+
* super(db, schema, schema.user);
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
*/
|
|
111
|
+
class BaseTableRepository extends BaseRepository {
|
|
112
|
+
table;
|
|
113
|
+
idKey;
|
|
114
|
+
idColumn;
|
|
115
|
+
constructor(options, repository) {
|
|
116
|
+
super({ orm: options.orm, schema: options.schema }, repository);
|
|
117
|
+
this.table = options.table;
|
|
118
|
+
this.idKey = options.idKey ?? "id";
|
|
119
|
+
this.idColumn = this.table[this.idKey];
|
|
120
|
+
}
|
|
121
|
+
withSorting(query, { sort, order }, table) {
|
|
122
|
+
return super.withSorting(query, { sort, order }, table || this.table);
|
|
123
|
+
}
|
|
124
|
+
withSortingAndPagination(query, { sort, order, page, limit }, table) {
|
|
125
|
+
return super.withSortingAndPagination(query, { sort, order, page, limit }, table || this.table);
|
|
126
|
+
}
|
|
127
|
+
async queryList(query, options, tx) {
|
|
128
|
+
return this.throwableAsync(async () => {
|
|
129
|
+
const db = tx ?? this.orm;
|
|
130
|
+
const conditions = options?.conditions ?? this.getConditionBuilder(this.table);
|
|
131
|
+
conditions.applyFilters(query);
|
|
132
|
+
const whereClause = conditions.join();
|
|
133
|
+
const rowsQuery = this.withSortingAndPagination((options?.select ? db.select(options.select) : db.select())
|
|
134
|
+
.from(this.table)
|
|
135
|
+
.where(whereClause), query || {});
|
|
136
|
+
const countQuery = db
|
|
137
|
+
.select({ count: (0, drizzle_orm_1.count)() })
|
|
138
|
+
.from(this.table)
|
|
139
|
+
.where(whereClause);
|
|
140
|
+
const [rows, [totalResult]] = await Promise.all([rowsQuery, countQuery]);
|
|
141
|
+
return (0, neverthrow_1.ok)({ rows: rows, total: totalResult?.count ?? 0 });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async findById(id, tx) {
|
|
145
|
+
return this.throwableAsync(async () => {
|
|
146
|
+
const db = tx ?? this.orm;
|
|
147
|
+
const rows = (await db
|
|
148
|
+
.select()
|
|
149
|
+
.from(this.table)
|
|
150
|
+
.where((0, drizzle_orm_1.eq)(this.idColumn, id)));
|
|
151
|
+
return (0, neverthrow_1.ok)(rows[0]);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async findManyById(ids, tx) {
|
|
155
|
+
return this.throwableAsync(async () => {
|
|
156
|
+
const db = tx ?? this.orm;
|
|
157
|
+
if (ids.length === 0) {
|
|
158
|
+
return (0, neverthrow_1.ok)([]);
|
|
159
|
+
}
|
|
160
|
+
const rows = (await db
|
|
161
|
+
.select()
|
|
162
|
+
.from(this.table)
|
|
163
|
+
.where((0, drizzle_orm_1.inArray)(this.idColumn, ids)));
|
|
164
|
+
return (0, neverthrow_1.ok)(rows);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async create(data, tx) {
|
|
168
|
+
return this.throwableAsync(async () => {
|
|
169
|
+
const db = tx ?? this.orm;
|
|
170
|
+
const rows = (await db
|
|
171
|
+
.insert(this.table)
|
|
172
|
+
.values(data)
|
|
173
|
+
.returning());
|
|
174
|
+
if (rows.length === 0)
|
|
175
|
+
return this.error("UNPROCESSABLE_CONTENT");
|
|
176
|
+
return (0, neverthrow_1.ok)(rows[0]);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async createMany(data, tx) {
|
|
180
|
+
return this.throwableAsync(async () => {
|
|
181
|
+
const db = tx ?? this.orm;
|
|
182
|
+
if (data.length === 0) {
|
|
183
|
+
return (0, neverthrow_1.ok)([]);
|
|
184
|
+
}
|
|
185
|
+
const rows = (await db
|
|
186
|
+
.insert(this.table)
|
|
187
|
+
.values(data)
|
|
188
|
+
.returning());
|
|
189
|
+
return (0, neverthrow_1.ok)(rows);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async update(data, tx) {
|
|
193
|
+
return this.throwableAsync(async () => {
|
|
194
|
+
const db = tx ?? this.orm;
|
|
195
|
+
const single = data;
|
|
196
|
+
const id = String(single[this.idKey]);
|
|
197
|
+
const { [this.idKey]: _removed, ...rest } = single;
|
|
198
|
+
const update = rest;
|
|
199
|
+
if (this.table.updatedAt)
|
|
200
|
+
update.updatedAt = new Date();
|
|
201
|
+
const rows = (await db
|
|
202
|
+
.update(this.table)
|
|
203
|
+
.set(update)
|
|
204
|
+
.where((0, drizzle_orm_1.eq)(this.idColumn, id))
|
|
205
|
+
.returning());
|
|
206
|
+
const [row] = rows;
|
|
207
|
+
if (!row)
|
|
208
|
+
return this.error("NOT_FOUND");
|
|
209
|
+
return (0, neverthrow_1.ok)(row);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async updateMany(data, tx) {
|
|
213
|
+
return this.throwableAsync(async () => {
|
|
214
|
+
const db = tx ?? this.orm;
|
|
215
|
+
if (data.length === 0) {
|
|
216
|
+
return (0, neverthrow_1.ok)([]);
|
|
217
|
+
}
|
|
218
|
+
const results = [];
|
|
219
|
+
for (const item of data) {
|
|
220
|
+
const record = item;
|
|
221
|
+
const id = String(record[this.idKey]);
|
|
222
|
+
const { [this.idKey]: _removed, ...rest } = record;
|
|
223
|
+
const update = rest;
|
|
224
|
+
if (this.table.updatedAt)
|
|
225
|
+
update.updatedAt = new Date();
|
|
226
|
+
const rows = (await db
|
|
227
|
+
.update(this.table)
|
|
228
|
+
.set(update)
|
|
229
|
+
.where((0, drizzle_orm_1.eq)(this.idColumn, id))
|
|
230
|
+
.returning());
|
|
231
|
+
if (rows[0])
|
|
232
|
+
results.push(rows[0]);
|
|
233
|
+
}
|
|
234
|
+
return (0, neverthrow_1.ok)(results);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async softDeleteById(id, tx) {
|
|
238
|
+
return this.throwableAsync(async () => {
|
|
239
|
+
const db = tx ?? this.orm;
|
|
240
|
+
if (!this.table.deletedAt)
|
|
241
|
+
return this.error("METHOD_NOT_SUPPORTED");
|
|
242
|
+
const rows = await db
|
|
243
|
+
.update(this.table)
|
|
244
|
+
.set({ deletedAt: new Date() })
|
|
245
|
+
.where((0, drizzle_orm_1.eq)(this.idColumn, id))
|
|
246
|
+
.returning({
|
|
247
|
+
id: this.idColumn,
|
|
248
|
+
});
|
|
249
|
+
if (rows.length === 0)
|
|
250
|
+
return this.error("NOT_FOUND");
|
|
251
|
+
return (0, neverthrow_1.ok)(rows[0]);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async softDeleteManyById(ids, tx) {
|
|
255
|
+
return this.throwableAsync(async () => {
|
|
256
|
+
const db = tx ?? this.orm;
|
|
257
|
+
if (!this.table.deletedAt)
|
|
258
|
+
return this.error("METHOD_NOT_SUPPORTED");
|
|
259
|
+
const rows = await db
|
|
260
|
+
.update(this.table)
|
|
261
|
+
.set({ deletedAt: new Date() })
|
|
262
|
+
.where((0, drizzle_orm_1.inArray)(this.idColumn, ids))
|
|
263
|
+
.returning({
|
|
264
|
+
id: this.idColumn,
|
|
265
|
+
});
|
|
266
|
+
if (rows.length === 0)
|
|
267
|
+
return this.error("NOT_FOUND");
|
|
268
|
+
return (0, neverthrow_1.ok)(rows);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async deleteById(id, tx) {
|
|
272
|
+
return this.throwableAsync(async () => {
|
|
273
|
+
const db = tx ?? this.orm;
|
|
274
|
+
const rows = await db
|
|
275
|
+
.delete(this.table)
|
|
276
|
+
.where((0, drizzle_orm_1.eq)(this.idColumn, id))
|
|
277
|
+
.returning({
|
|
278
|
+
id: this.idColumn,
|
|
279
|
+
});
|
|
280
|
+
if (rows.length === 0)
|
|
281
|
+
return this.error("NOT_FOUND");
|
|
282
|
+
return (0, neverthrow_1.ok)(rows[0]);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async deleteManyById(ids, tx) {
|
|
286
|
+
return this.throwableAsync(async () => {
|
|
287
|
+
const db = tx ?? this.orm;
|
|
288
|
+
if (ids.length === 0) {
|
|
289
|
+
return (0, neverthrow_1.ok)([]);
|
|
290
|
+
}
|
|
291
|
+
const rows = await db
|
|
292
|
+
.delete(this.table)
|
|
293
|
+
.where((0, drizzle_orm_1.inArray)(this.idColumn, ids))
|
|
294
|
+
.returning({
|
|
295
|
+
id: this.idColumn,
|
|
296
|
+
});
|
|
297
|
+
return (0, neverthrow_1.ok)(rows);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
exports.BaseTableRepository = BaseTableRepository;
|
|
302
|
+
class BaseExternaRepository extends base_abstract_1.Base {
|
|
303
|
+
constructor() {
|
|
304
|
+
super("repository");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.BaseExternaRepository = BaseExternaRepository;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BasePermissionService = exports.BaseService = void 0;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const base_abstract_1 = require("#modules/base/base.abstract");
|
|
6
|
+
const base_grants_1 = require("#modules/base/base.grants");
|
|
7
|
+
class BaseService extends base_abstract_1.Base {
|
|
8
|
+
repository;
|
|
9
|
+
service;
|
|
10
|
+
constructor(repository = {}, service = {}) {
|
|
11
|
+
super("service");
|
|
12
|
+
this.repository = repository;
|
|
13
|
+
this.service = service;
|
|
14
|
+
this.repository = repository;
|
|
15
|
+
this.service = service;
|
|
16
|
+
}
|
|
17
|
+
addUserFilter(value, query, columnId = "userId", method = "equals") {
|
|
18
|
+
const userFilter = {
|
|
19
|
+
columnId,
|
|
20
|
+
type: "string",
|
|
21
|
+
method,
|
|
22
|
+
value,
|
|
23
|
+
};
|
|
24
|
+
return query
|
|
25
|
+
? { ...query, filters: [...(query?.filters ?? []), userFilter] }
|
|
26
|
+
: { filters: [userFilter] };
|
|
27
|
+
}
|
|
28
|
+
addContextFilter(ctx, include = {
|
|
29
|
+
user: true,
|
|
30
|
+
organization: false,
|
|
31
|
+
team: false,
|
|
32
|
+
}, query, map = {
|
|
33
|
+
userId: {
|
|
34
|
+
columnId: "userId",
|
|
35
|
+
method: "equals",
|
|
36
|
+
},
|
|
37
|
+
organizationId: {
|
|
38
|
+
columnId: "organizationId",
|
|
39
|
+
method: "equals",
|
|
40
|
+
},
|
|
41
|
+
teamId: {
|
|
42
|
+
columnId: "teamId",
|
|
43
|
+
method: "equals",
|
|
44
|
+
},
|
|
45
|
+
}) {
|
|
46
|
+
const filters = [];
|
|
47
|
+
if (include.user) {
|
|
48
|
+
filters.push({
|
|
49
|
+
columnId: map.userId.columnId,
|
|
50
|
+
type: "string",
|
|
51
|
+
method: map.userId.method,
|
|
52
|
+
value: ctx.user.id,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (include.organization) {
|
|
56
|
+
filters.push({
|
|
57
|
+
columnId: map.organizationId.columnId,
|
|
58
|
+
type: "string",
|
|
59
|
+
method: map.organizationId.method,
|
|
60
|
+
value: ctx.session.activeOrganizationId ?? "",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (include.team) {
|
|
64
|
+
filters.push({
|
|
65
|
+
columnId: map.teamId.columnId,
|
|
66
|
+
type: "string",
|
|
67
|
+
method: map.teamId.method,
|
|
68
|
+
value: ctx.session.activeTeamId ?? "",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return query ? { ...query, filters: [...(query?.filters ?? []), ...filters] } : { filters };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.BaseService = BaseService;
|
|
75
|
+
class BasePermissionService extends BaseService {
|
|
76
|
+
grants;
|
|
77
|
+
constructor(repository, service, grants = []) {
|
|
78
|
+
super(repository, service);
|
|
79
|
+
this.grants = grants;
|
|
80
|
+
}
|
|
81
|
+
accessGuard(ctx, action, entities, grants) {
|
|
82
|
+
const hasPermission = this.checkPermission(ctx, action, entities, grants);
|
|
83
|
+
if (!hasPermission)
|
|
84
|
+
return this.error("FORBIDDEN");
|
|
85
|
+
return (0, neverthrow_1.ok)(true);
|
|
86
|
+
}
|
|
87
|
+
async accessGuardAsync(ctx, action, getEntities, grants) {
|
|
88
|
+
const hasPermission = await this.checkPermissionAsync(ctx, action, getEntities, grants);
|
|
89
|
+
if (hasPermission.isErr())
|
|
90
|
+
return (0, neverthrow_1.err)(hasPermission.error);
|
|
91
|
+
if (!hasPermission.value)
|
|
92
|
+
return this.error("FORBIDDEN");
|
|
93
|
+
return (0, neverthrow_1.ok)(true);
|
|
94
|
+
}
|
|
95
|
+
checkPermission(ctx, action, entities, grants) {
|
|
96
|
+
const actionGrants = grants ?? this.grants.filter((grant) => grant.action === action);
|
|
97
|
+
return (0, base_grants_1.checkPermissionSync)(ctx, actionGrants, entities);
|
|
98
|
+
}
|
|
99
|
+
async checkPermissionAsync(ctx, action, getEntities, grants) {
|
|
100
|
+
const actionGrants = grants ?? this.grants.filter((grant) => grant.action === action);
|
|
101
|
+
const permission = await (0, base_grants_1.checkPermissionAsync)(ctx, actionGrants, getEntities);
|
|
102
|
+
if (permission.isErr())
|
|
103
|
+
return this.error("INTERNAL_SERVER_ERROR", "Failed to check permission", {
|
|
104
|
+
cause: permission.error,
|
|
105
|
+
});
|
|
106
|
+
return (0, neverthrow_1.ok)(permission.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.BasePermissionService = BasePermissionService;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subscriptions = void 0;
|
|
4
|
+
const sqlite_core_1 = require("drizzle-orm/sqlite-core");
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
exports.subscriptions = (0, sqlite_core_1.sqliteTable)("subscriptions", {
|
|
7
|
+
id: (0, sqlite_core_1.text)("id").primaryKey().$default(uuid_1.v4),
|
|
8
|
+
createdAt: (0, sqlite_core_1.integer)("created_at", { mode: "timestamp" })
|
|
9
|
+
.notNull()
|
|
10
|
+
.$default(() => new Date()),
|
|
11
|
+
updatedAt: (0, sqlite_core_1.integer)("updated_at", { mode: "timestamp" }),
|
|
12
|
+
plan: (0, sqlite_core_1.text)("plan").notNull(),
|
|
13
|
+
referenceId: (0, sqlite_core_1.text)("reference_id").notNull(),
|
|
14
|
+
stripeCustomerId: (0, sqlite_core_1.text)("stripe_customer_id"),
|
|
15
|
+
stripeSubscriptionId: (0, sqlite_core_1.text)("stripe_subscription_id"),
|
|
16
|
+
status: (0, sqlite_core_1.text)("status").notNull(),
|
|
17
|
+
periodStart: (0, sqlite_core_1.integer)("period_start", { mode: "timestamp" }),
|
|
18
|
+
periodEnd: (0, sqlite_core_1.integer)("period_end", { mode: "timestamp" }),
|
|
19
|
+
priceId: (0, sqlite_core_1.text)("price_id"),
|
|
20
|
+
interval: (0, sqlite_core_1.text)("interval"),
|
|
21
|
+
unitAmount: (0, sqlite_core_1.integer)("unit_amount", { mode: "number" }),
|
|
22
|
+
discounts: (0, sqlite_core_1.text)("discounts", { mode: "json" }).$type(),
|
|
23
|
+
cancelAtPeriodEnd: (0, sqlite_core_1.integer)("cancel_at_period_end", { mode: "boolean" }),
|
|
24
|
+
cancelAt: (0, sqlite_core_1.integer)("cancel_at", { mode: "timestamp" }),
|
|
25
|
+
canceledAt: (0, sqlite_core_1.integer)("canceled_at", { mode: "timestamp" }),
|
|
26
|
+
seats: (0, sqlite_core_1.integer)("seats", { mode: "number" }),
|
|
27
|
+
trialStart: (0, sqlite_core_1.integer)("trial_start", { mode: "timestamp" }),
|
|
28
|
+
trialEnd: (0, sqlite_core_1.integer)("trial_end", { mode: "timestamp" }),
|
|
29
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BillingRepository = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
6
|
+
const neverthrow_1 = require("neverthrow");
|
|
7
|
+
const auth = tslib_1.__importStar(require("#modules/auth/auth.db"));
|
|
8
|
+
const base_repository_1 = require("#modules/base/base.repository");
|
|
9
|
+
const billing = tslib_1.__importStar(require("#modules/billing/billing.db"));
|
|
10
|
+
const posthog_1 = require("#utils/posthog");
|
|
11
|
+
const schema = { ...auth, ...billing };
|
|
12
|
+
class BillingRepository extends base_repository_1.BaseTableRepository {
|
|
13
|
+
stripe;
|
|
14
|
+
plans;
|
|
15
|
+
trial;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
const { libs, config, ...rest } = options;
|
|
18
|
+
super(rest);
|
|
19
|
+
this.stripe = libs.stripe;
|
|
20
|
+
this.plans = config.plans;
|
|
21
|
+
this.trial = config.trial;
|
|
22
|
+
}
|
|
23
|
+
hasTrial() {
|
|
24
|
+
return !!this.trial;
|
|
25
|
+
}
|
|
26
|
+
getPlanByPriceId(priceId) {
|
|
27
|
+
return this.plans.find((plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId);
|
|
28
|
+
}
|
|
29
|
+
getCustomerByEmail(email) {
|
|
30
|
+
return this.throwableAsync(async () => {
|
|
31
|
+
const customers = await this.stripe.customers.list({
|
|
32
|
+
email,
|
|
33
|
+
limit: 1,
|
|
34
|
+
});
|
|
35
|
+
return (0, neverthrow_1.ok)(customers.data[0] ?? null);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
getUserByCustomerId(customerId) {
|
|
39
|
+
return this.throwableAsync(async () => {
|
|
40
|
+
const [user] = await this.orm
|
|
41
|
+
.select()
|
|
42
|
+
.from(this.schema.users)
|
|
43
|
+
.where((0, drizzle_orm_1.eq)(this.schema.users.stripeCustomerId, customerId))
|
|
44
|
+
.limit(1);
|
|
45
|
+
return (0, neverthrow_1.ok)(user ?? null);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
createCustomer({ email, name, userId, }) {
|
|
49
|
+
return this.throwableAsync(async () => {
|
|
50
|
+
const customer = await this.stripe.customers.create({
|
|
51
|
+
email,
|
|
52
|
+
name,
|
|
53
|
+
metadata: {
|
|
54
|
+
userId,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return (0, neverthrow_1.ok)(customer);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async createTrialSubscription(customerId) {
|
|
61
|
+
if (!this.trial)
|
|
62
|
+
return this.error("NOT_FOUND", "Trial plan not found");
|
|
63
|
+
const stripeSubscription = await this.createSubscription({
|
|
64
|
+
customerId,
|
|
65
|
+
priceId: this.trial.priceId,
|
|
66
|
+
trialDays: this.trial.freeTrial?.days ?? 7,
|
|
67
|
+
});
|
|
68
|
+
if (stripeSubscription.isErr())
|
|
69
|
+
return (0, neverthrow_1.err)(stripeSubscription.error);
|
|
70
|
+
if (!stripeSubscription.value)
|
|
71
|
+
return this.error("INTERNAL_SERVER_ERROR", "Failed to create trial subscription");
|
|
72
|
+
return (0, neverthrow_1.ok)(stripeSubscription.value);
|
|
73
|
+
}
|
|
74
|
+
createSubscription({ customerId, priceId, quantity = 1, trialDays, }) {
|
|
75
|
+
return this.throwableAsync(async () => {
|
|
76
|
+
const stripeSubscription = await this.stripe.subscriptions.create({
|
|
77
|
+
customer: customerId,
|
|
78
|
+
items: [{ price: priceId, quantity }], // quantity = seats if you want
|
|
79
|
+
...(trialDays
|
|
80
|
+
? {
|
|
81
|
+
trial_period_days: trialDays,
|
|
82
|
+
trial_settings: {
|
|
83
|
+
end_behavior: {
|
|
84
|
+
missing_payment_method: "cancel",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
});
|
|
90
|
+
return (0, neverthrow_1.ok)(stripeSubscription);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
updateUserCustomerId({ userId, customerId, }) {
|
|
94
|
+
return this.throwableAsync(async () => {
|
|
95
|
+
const [user] = await this.orm
|
|
96
|
+
.update(this.schema.users)
|
|
97
|
+
.set({ stripeCustomerId: customerId })
|
|
98
|
+
.where((0, drizzle_orm_1.eq)(this.schema.users.id, userId))
|
|
99
|
+
.returning();
|
|
100
|
+
if (!user)
|
|
101
|
+
return this.error("NOT_FOUND", "User not found");
|
|
102
|
+
return (0, neverthrow_1.ok)(user);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
getLatestSubscription(referenceId) {
|
|
106
|
+
return this.throwableAsync(async () => {
|
|
107
|
+
const subscriptions = await this.orm
|
|
108
|
+
.select()
|
|
109
|
+
.from(this.schema.subscriptions)
|
|
110
|
+
.where((0, drizzle_orm_1.eq)(this.schema.subscriptions.referenceId, referenceId))
|
|
111
|
+
.orderBy((0, drizzle_orm_1.desc)(this.schema.subscriptions.createdAt))
|
|
112
|
+
.limit(1);
|
|
113
|
+
return (0, neverthrow_1.ok)(subscriptions[0] ?? null);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
getActiveSubscription(referenceId) {
|
|
117
|
+
return this.throwableAsync(async () => {
|
|
118
|
+
const [subscription] = await this.orm
|
|
119
|
+
.select()
|
|
120
|
+
.from(this.schema.subscriptions)
|
|
121
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(this.schema.subscriptions.referenceId, referenceId), (0, drizzle_orm_1.inArray)(this.schema.subscriptions.status, ["active", "trialing"])))
|
|
122
|
+
.orderBy((0, drizzle_orm_1.desc)(this.schema.subscriptions.createdAt))
|
|
123
|
+
.limit(1);
|
|
124
|
+
return (0, neverthrow_1.ok)(subscription ?? null);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
listInvoices(customerId) {
|
|
128
|
+
return this.throwableAsync(async () => {
|
|
129
|
+
const invoices = await this.stripe.invoices.list({
|
|
130
|
+
customer: customerId,
|
|
131
|
+
});
|
|
132
|
+
return (0, neverthrow_1.ok)(invoices.data);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
createCheckoutSession({ customerId, priceId, userId, }) {
|
|
136
|
+
return this.throwableAsync(async () => {
|
|
137
|
+
const session = await this.stripe.checkout.sessions.create({
|
|
138
|
+
client_reference_id: userId,
|
|
139
|
+
customer: customerId,
|
|
140
|
+
success_url: `${process.env.VITE_SERVER_URL}/stripe/success`,
|
|
141
|
+
cancel_url: `${process.env.VITE_APP_URL}/billing`,
|
|
142
|
+
mode: "subscription",
|
|
143
|
+
line_items: [
|
|
144
|
+
{
|
|
145
|
+
price: priceId,
|
|
146
|
+
quantity: 1,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
return (0, neverthrow_1.ok)(session);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
createBillingPortalSession(customerId) {
|
|
154
|
+
return this.throwableAsync(async () => {
|
|
155
|
+
const session = await this.stripe.billingPortal.sessions.create({
|
|
156
|
+
customer: customerId,
|
|
157
|
+
return_url: `${process.env.VITE_SERVER_URL}/stripe/success`,
|
|
158
|
+
});
|
|
159
|
+
return (0, neverthrow_1.ok)(session);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async syncStripeData({ customerId, userId, }) {
|
|
163
|
+
return this.throwableAsync(async () => {
|
|
164
|
+
// Fetch latest subscription data from Stripe
|
|
165
|
+
const stripeSubscriptions = await this.stripe.subscriptions.list({
|
|
166
|
+
customer: customerId,
|
|
167
|
+
limit: 1,
|
|
168
|
+
status: "all",
|
|
169
|
+
expand: ["data.default_payment_method"],
|
|
170
|
+
});
|
|
171
|
+
const [stripeSubscription] = stripeSubscriptions.data;
|
|
172
|
+
if (!stripeSubscription)
|
|
173
|
+
return this.error("NOT_FOUND", "Subscription not found");
|
|
174
|
+
const plan = this.getPlanByPriceId(stripeSubscription.items.data[0]?.price.id);
|
|
175
|
+
if (!plan)
|
|
176
|
+
return this.error("NOT_FOUND", `Plan not found for price ID: ${stripeSubscription.items.data[0]?.price.id}`);
|
|
177
|
+
const values = {
|
|
178
|
+
stripeCustomerId: customerId,
|
|
179
|
+
referenceId: userId,
|
|
180
|
+
plan: plan.name,
|
|
181
|
+
status: stripeSubscription.status,
|
|
182
|
+
seats: stripeSubscription.items.data[0]?.quantity || 1,
|
|
183
|
+
periodEnd: new Date(stripeSubscription.items.data[0]?.current_period_end * 1000),
|
|
184
|
+
periodStart: new Date(stripeSubscription.items.data[0]?.current_period_start * 1000),
|
|
185
|
+
priceId: stripeSubscription.items.data[0]?.price.id,
|
|
186
|
+
interval: stripeSubscription.items.data[0]?.price.recurring?.interval,
|
|
187
|
+
unitAmount: stripeSubscription.items.data[0]?.price.unit_amount,
|
|
188
|
+
discounts: stripeSubscription.discounts.map((discount) => typeof discount === "string" ? discount : discount.id),
|
|
189
|
+
stripeSubscriptionId: stripeSubscription.id,
|
|
190
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
191
|
+
cancelAt: stripeSubscription.cancel_at
|
|
192
|
+
? new Date(stripeSubscription.cancel_at * 1000)
|
|
193
|
+
: null,
|
|
194
|
+
canceledAt: stripeSubscription.canceled_at
|
|
195
|
+
? new Date(stripeSubscription.canceled_at * 1000)
|
|
196
|
+
: null,
|
|
197
|
+
...(stripeSubscription.trial_start && stripeSubscription.trial_end
|
|
198
|
+
? {
|
|
199
|
+
trialStart: new Date(stripeSubscription.trial_start * 1000),
|
|
200
|
+
trialEnd: new Date(stripeSubscription.trial_end * 1000),
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
};
|
|
204
|
+
const existingSubscription = await this.getActiveSubscription(userId);
|
|
205
|
+
if (existingSubscription.isErr())
|
|
206
|
+
return (0, neverthrow_1.err)(existingSubscription.error);
|
|
207
|
+
if (!existingSubscription.value) {
|
|
208
|
+
await this.orm.insert(this.schema.subscriptions).values(values);
|
|
209
|
+
(0, posthog_1.posthogCapture)({
|
|
210
|
+
distinctId: userId,
|
|
211
|
+
event: "stripe.subscription_created",
|
|
212
|
+
properties: values,
|
|
213
|
+
});
|
|
214
|
+
return (0, neverthrow_1.ok)(true);
|
|
215
|
+
}
|
|
216
|
+
await this.orm
|
|
217
|
+
.update(this.schema.subscriptions)
|
|
218
|
+
.set({ ...values, updatedAt: new Date() })
|
|
219
|
+
.where((0, drizzle_orm_1.eq)(this.schema.subscriptions.id, existingSubscription.value.id));
|
|
220
|
+
(0, posthog_1.posthogCapture)({
|
|
221
|
+
distinctId: userId,
|
|
222
|
+
event: "stripe.subscription_updated",
|
|
223
|
+
properties: values,
|
|
224
|
+
});
|
|
225
|
+
return (0, neverthrow_1.ok)(false);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
constructEvent(body, signature, secret) {
|
|
229
|
+
return this.throwable(() => {
|
|
230
|
+
const event = this.stripe.webhooks.constructEvent(body, signature, secret);
|
|
231
|
+
return (0, neverthrow_1.ok)(event);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.BillingRepository = BillingRepository;
|