@yimingliao/cms 0.0.7 → 0.0.9

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.
@@ -0,0 +1,1242 @@
1
+ import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-KEQXXUK2.js';
2
+ import jwt from 'jsonwebtoken';
3
+ import argon2, { argon2id } from 'argon2';
4
+ import crypto, { timingSafeEqual } from 'crypto';
5
+ import 'next/headers';
6
+ import { ulid } from 'ulid';
7
+
8
+ function createJwtService(config) {
9
+ const { defaultSecret, ...options } = config;
10
+ function sign({
11
+ payload = {},
12
+ secret = defaultSecret,
13
+ expiresIn = 60 * 60
14
+ }) {
15
+ return jwt.sign(payload, secret, {
16
+ algorithm: "HS256",
17
+ expiresIn,
18
+ ...options
19
+ });
20
+ }
21
+ function verify({
22
+ token,
23
+ secret = defaultSecret
24
+ }) {
25
+ const payload = jwt.verify(token, secret, {
26
+ algorithms: ["HS256"],
27
+ ...options
28
+ });
29
+ if (typeof payload === "string") {
30
+ throw new TypeError("Invalid JWT payload");
31
+ }
32
+ return payload;
33
+ }
34
+ return {
35
+ sign,
36
+ verify
37
+ };
38
+ }
39
+ var OPTIONS = {
40
+ type: argon2id,
41
+ memoryCost: 19456,
42
+ // ~19MB
43
+ timeCost: 2,
44
+ parallelism: 1
45
+ };
46
+ function createArgon2Service() {
47
+ async function hash(password) {
48
+ return await argon2.hash(password, OPTIONS);
49
+ }
50
+ async function verify(hash2, plain) {
51
+ return await argon2.verify(hash2, plain);
52
+ }
53
+ return {
54
+ hash,
55
+ verify
56
+ };
57
+ }
58
+ function createCryptoService(config) {
59
+ const { defaultSecret } = config;
60
+ const SECRET = crypto.createHash("sha256").update(defaultSecret).digest();
61
+ const ALGORITHM = "aes-256-gcm";
62
+ const IV_LENGTH = 12;
63
+ function generateToken() {
64
+ return crypto.randomBytes(32).toString("base64url");
65
+ }
66
+ function hash(value) {
67
+ return crypto.createHash("sha256").update(value).digest("hex");
68
+ }
69
+ function hashBuffer(value) {
70
+ return crypto.createHash("sha256").update(value).digest();
71
+ }
72
+ function encrypt(data) {
73
+ const iv = crypto.randomBytes(IV_LENGTH);
74
+ const cipher = crypto.createCipheriv(ALGORITHM, SECRET, iv);
75
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
76
+ const tag = cipher.getAuthTag();
77
+ return [
78
+ iv.toString("hex"),
79
+ encrypted.toString("hex"),
80
+ tag.toString("hex")
81
+ ].join(":");
82
+ }
83
+ function decrypt(data) {
84
+ const parts = data.split(":");
85
+ if (parts.length !== 3 || !parts.every(Boolean)) {
86
+ throw new Error("Invalid encrypted payload");
87
+ }
88
+ const [ivHex, encryptedHex, tagHex] = parts;
89
+ const iv = Buffer.from(ivHex, "hex");
90
+ const encrypted = Buffer.from(encryptedHex, "hex");
91
+ const tag = Buffer.from(tagHex, "hex");
92
+ const decipher = crypto.createDecipheriv(ALGORITHM, SECRET, iv);
93
+ decipher.setAuthTag(tag);
94
+ const decrypted = Buffer.concat([
95
+ decipher.update(encrypted),
96
+ decipher.final()
97
+ ]);
98
+ return decrypted.toString("utf8");
99
+ }
100
+ function sign({
101
+ value,
102
+ expireSeconds,
103
+ createdAt
104
+ }) {
105
+ const payload = `${value}|${expireSeconds}|${createdAt ?? Date.now()}`;
106
+ const signature = crypto.createHmac("sha256", SECRET).update(payload).digest("hex");
107
+ const signed = `${payload}|${signature}`;
108
+ return { signed, signature };
109
+ }
110
+ return {
111
+ generateToken,
112
+ hash,
113
+ hashBuffer,
114
+ encrypt,
115
+ decrypt,
116
+ sign
117
+ };
118
+ }
119
+ var DEFAULTS = {
120
+ httpOnly: true,
121
+ secure: process.env["NODE_ENV"] === "production",
122
+ sameSite: "strict",
123
+ path: "/"
124
+ };
125
+ function createCookieService(nextCookies, cryptoService) {
126
+ async function set({
127
+ name,
128
+ value,
129
+ expireSeconds
130
+ }) {
131
+ const cookieStore = await nextCookies();
132
+ cookieStore.set(name, value, { ...DEFAULTS, maxAge: expireSeconds });
133
+ }
134
+ async function setSignedCookie({
135
+ name,
136
+ value,
137
+ expireSeconds
138
+ }) {
139
+ const createdAt = Date.now();
140
+ const { signed } = cryptoService.sign({
141
+ value,
142
+ expireSeconds,
143
+ createdAt
144
+ });
145
+ await set({ name, value: signed, expireSeconds });
146
+ }
147
+ async function get({ name }) {
148
+ const cookieStore = await nextCookies();
149
+ const cookieValue = cookieStore.get(name)?.value;
150
+ return cookieValue;
151
+ }
152
+ async function getSignedCookie({ name }) {
153
+ const signed = await get({ name });
154
+ const value = verifySignedCookie({ signed });
155
+ return value;
156
+ }
157
+ function verifySignedCookie({ signed }) {
158
+ if (!signed) throw new Error("Invalid cookie");
159
+ const parts = signed.split("|");
160
+ if (parts.length !== 4) throw new Error("Invalid cookie");
161
+ const [value, expireSecondsStr, createdAtStr, signature] = parts;
162
+ const expireSeconds = Number.parseInt(expireSecondsStr, 10);
163
+ const createdAt = Number.parseInt(createdAtStr, 10);
164
+ if (Number.isNaN(expireSeconds) || Number.isNaN(createdAt)) {
165
+ throw new TypeError("Invalid cookie");
166
+ }
167
+ const now = Date.now();
168
+ if (now > createdAt + expireSeconds * 1e3) {
169
+ throw new Error("Cookie expired");
170
+ }
171
+ const { signature: generatedSignature } = cryptoService.sign({
172
+ value,
173
+ expireSeconds,
174
+ createdAt
175
+ });
176
+ const sig = Buffer.from(signature);
177
+ const gen = Buffer.from(generatedSignature);
178
+ if (sig.length !== gen.length || !timingSafeEqual(sig, gen)) {
179
+ throw new Error("Invalid cookie signature");
180
+ }
181
+ return value;
182
+ }
183
+ async function deleteCokkie({ name }) {
184
+ const cookieStore = await nextCookies();
185
+ cookieStore.delete(name);
186
+ }
187
+ return {
188
+ set,
189
+ setSignedCookie,
190
+ get,
191
+ getSignedCookie,
192
+ verifySignedCookie,
193
+ delete: deleteCokkie
194
+ };
195
+ }
196
+
197
+ // src/server/infrastructure/database/utils/connect.ts
198
+ var ids = (items) => items.map(({ id }) => ({ id }));
199
+ function connectOne(item) {
200
+ return item ? { connect: { id: item.id } } : {};
201
+ }
202
+ function connectMany(items) {
203
+ return { connect: ids(items) };
204
+ }
205
+ function updateOne(item) {
206
+ return item ? { connect: { id: item.id } } : { disconnect: true };
207
+ }
208
+ function updateMany(items) {
209
+ return { set: ids(items) };
210
+ }
211
+
212
+ // src/server/infrastructure/database/admin/command/create-admin-command-repository.ts
213
+ function createAdminCommandRepository(prisma) {
214
+ async function create({
215
+ // ------------------------------------
216
+ // relations
217
+ // ------------------------------------
218
+ // File
219
+ avatarImage,
220
+ // ------------------------------------
221
+ // translation
222
+ // ------------------------------------
223
+ translations,
224
+ // rest
225
+ ...params
226
+ }) {
227
+ const created = await prisma.admin.create({
228
+ data: {
229
+ ...params,
230
+ // ------------------------------------------------------------------------
231
+ // relations
232
+ // ------------------------------------------------------------------------
233
+ // File
234
+ avatarImage: connectOne(avatarImage),
235
+ // ------------------------------------------------------------------------
236
+ // translation
237
+ // ------------------------------------------------------------------------
238
+ translations: { create: translations }
239
+ }
240
+ });
241
+ return created;
242
+ }
243
+ async function update({
244
+ id,
245
+ // ------------------------------------
246
+ // relations
247
+ // ------------------------------------
248
+ // File
249
+ avatarImage,
250
+ // ------------------------------------
251
+ // translation
252
+ // ------------------------------------
253
+ translations,
254
+ // rest
255
+ ...params
256
+ }) {
257
+ const updated = await prisma.admin.update({
258
+ where: { id },
259
+ data: {
260
+ ...params,
261
+ // ------------------------------------------------------------------------
262
+ // relations
263
+ // ------------------------------------------------------------------------
264
+ // File
265
+ avatarImage: updateOne(avatarImage),
266
+ // ------------------------------------------------------------------------
267
+ // translation
268
+ // ------------------------------------------------------------------------
269
+ translations: {
270
+ upsert: translations.map((t) => ({
271
+ where: { adminId_locale: { adminId: id, locale: t.locale } },
272
+ update: t,
273
+ create: t
274
+ }))
275
+ }
276
+ }
277
+ });
278
+ return updated;
279
+ }
280
+ async function _delete({ id }) {
281
+ const deleted = await prisma.admin.delete({
282
+ where: { id }
283
+ });
284
+ return deleted;
285
+ }
286
+ return {
287
+ create,
288
+ update,
289
+ delete: _delete
290
+ };
291
+ }
292
+
293
+ // src/server/infrastructure/database/constants.ts
294
+ var ORDER_BY = [
295
+ { updatedAt: "desc" },
296
+ { createdAt: "desc" },
297
+ { id: "asc" }
298
+ ];
299
+ var ADMIN_ORDER_BY = [
300
+ { role: "asc" },
301
+ ...ORDER_BY
302
+ ];
303
+ var POST_ORDER_BY = [
304
+ { index: "asc" },
305
+ ...ORDER_BY
306
+ ];
307
+
308
+ // src/server/infrastructure/database/admin/include.ts
309
+ var ADMIN_FULL_INCLUDE = {
310
+ // ---------------------------
311
+ // relations: AdminRefreshToken
312
+ // ---------------------------
313
+ adminRefreshTokens: true,
314
+ // ---------------------------
315
+ // relations: File
316
+ // ---------------------------
317
+ avatarImage: { include: { translations: true } },
318
+ // ---------------------------
319
+ // relations: Post
320
+ // ---------------------------
321
+ posts: true,
322
+ // ---------------------------
323
+ // translation
324
+ // ---------------------------
325
+ translations: true
326
+ };
327
+
328
+ // src/server/infrastructure/database/utils/create-search.ts
329
+ function buildContainsOr(fields, value) {
330
+ return fields.map((field) => ({
331
+ [field]: { contains: value, mode: "insensitive" }
332
+ }));
333
+ }
334
+ function buildTranslationSearch(locale, fields, value) {
335
+ return {
336
+ translations: {
337
+ some: { locale, OR: buildContainsOr(fields, value) }
338
+ }
339
+ };
340
+ }
341
+ function createSearch({
342
+ locale,
343
+ searchString,
344
+ rootFields = [],
345
+ translationFields = []
346
+ }) {
347
+ if (!searchString) return {};
348
+ const conditions = [];
349
+ if (rootFields.length > 0) {
350
+ conditions.push(...buildContainsOr(rootFields, searchString));
351
+ }
352
+ if (translationFields.length > 0 && locale) {
353
+ conditions.push(
354
+ buildTranslationSearch(locale, translationFields, searchString)
355
+ );
356
+ }
357
+ return conditions.length > 0 ? { OR: conditions } : {};
358
+ }
359
+
360
+ // src/server/infrastructure/database/utils/create-pagination.ts
361
+ function createPagination(page, pageSize) {
362
+ if (!page || !pageSize) return {};
363
+ return {
364
+ skip: (page - 1) * pageSize,
365
+ take: pageSize
366
+ };
367
+ }
368
+
369
+ // src/server/infrastructure/database/admin/query/create-admin-query-repository.ts
370
+ var OMIT_PASSWORD = { omit: { passwordHash: true } };
371
+ function buildWhere(params) {
372
+ if (params.id) return { id: params.id };
373
+ if (params.email) return { email: params.email };
374
+ return;
375
+ }
376
+ function createAdminQueryRepository(prisma) {
377
+ async function findListCards({
378
+ locale,
379
+ // search
380
+ searchString,
381
+ role,
382
+ adminIds,
383
+ // pagination
384
+ page,
385
+ pageSize
386
+ }) {
387
+ const where = {
388
+ // search
389
+ ...createSearch({
390
+ ...searchString !== void 0 ? { searchString } : {},
391
+ locale,
392
+ translationFields: ["name"]
393
+ }),
394
+ // role
395
+ ...role !== ADMIN_ROLES.SUPER_ADMIN ? { role: { notIn: [ADMIN_ROLES.SUPER_ADMIN] } } : {},
396
+ // Specified ids
397
+ ...adminIds?.length ? { id: { in: adminIds } } : {}
398
+ };
399
+ const [items, total] = await prisma.$transaction([
400
+ prisma.admin.findMany({
401
+ where,
402
+ orderBy: ADMIN_ORDER_BY,
403
+ include: ADMIN_FULL_INCLUDE,
404
+ ...createPagination(page, pageSize),
405
+ ...OMIT_PASSWORD
406
+ }),
407
+ prisma.admin.count({ where })
408
+ ]);
409
+ return { items, total };
410
+ }
411
+ async function find(params) {
412
+ const where = buildWhere(params);
413
+ if (!where) return null;
414
+ return prisma.admin.findUnique({
415
+ where,
416
+ ...OMIT_PASSWORD
417
+ });
418
+ }
419
+ async function findFull(params) {
420
+ const where = buildWhere(params);
421
+ if (!where) return null;
422
+ return await prisma.admin.findUnique({
423
+ where,
424
+ include: ADMIN_FULL_INCLUDE,
425
+ ...OMIT_PASSWORD
426
+ });
427
+ }
428
+ async function findWithPasswordHash(params) {
429
+ const where = buildWhere(params);
430
+ if (!where) return null;
431
+ return prisma.admin.findUnique({
432
+ where
433
+ });
434
+ }
435
+ return {
436
+ findListCards,
437
+ find,
438
+ findFull,
439
+ findWithPasswordHash
440
+ };
441
+ }
442
+
443
+ // src/server/infrastructure/database/admin-refresh-token/command/create-admin-refresh-token-command-repository.ts
444
+ function createAdminRefreshTokenCommandRepository(prisma) {
445
+ async function create({
446
+ adminId,
447
+ ...params
448
+ }) {
449
+ const created = await prisma.adminRefreshToken.create({
450
+ data: {
451
+ ...params,
452
+ admin: { connect: { id: adminId } },
453
+ deviceInfo: params.deviceInfo
454
+ }
455
+ });
456
+ return created;
457
+ }
458
+ async function _delete({ id, tokenHash }) {
459
+ const where = id ? { id } : tokenHash ? { tokenHash } : void 0;
460
+ if (where) {
461
+ await prisma.adminRefreshToken.delete({
462
+ where
463
+ });
464
+ }
465
+ }
466
+ async function deleteManyByExpired() {
467
+ const { count } = await prisma.adminRefreshToken.deleteMany({
468
+ where: { expiresAt: { lt: /* @__PURE__ */ new Date() } }
469
+ });
470
+ return count;
471
+ }
472
+ return {
473
+ create,
474
+ delete: _delete,
475
+ deleteManyByExpired
476
+ };
477
+ }
478
+
479
+ // src/server/infrastructure/database/admin-refresh-token/query/create-admin-refresh-token-query-repository.ts
480
+ function createAdminRefreshTokenQueryRepository(prisma) {
481
+ async function findManyByAdminId({
482
+ adminId
483
+ }) {
484
+ return await prisma.adminRefreshToken.findMany({
485
+ where: { adminId },
486
+ orderBy: [{ createdAt: "desc" }, { id: "asc" }]
487
+ });
488
+ }
489
+ async function findByToken({
490
+ tokenHash
491
+ }) {
492
+ return await prisma.adminRefreshToken.findUnique({
493
+ where: { tokenHash }
494
+ });
495
+ }
496
+ return {
497
+ findManyByAdminId,
498
+ findByToken
499
+ };
500
+ }
501
+
502
+ // src/server/infrastructure/database/file/include.ts
503
+ var FILE_FULL_INCLUDE = {
504
+ // ---------------------------
505
+ // relations: Folder
506
+ // ---------------------------
507
+ folder: true,
508
+ // ---------------------------
509
+ // relations: Admin
510
+ // ---------------------------
511
+ adminAsAvatarImage: true,
512
+ // ---------------------------
513
+ // relations: Post
514
+ // ---------------------------
515
+ postsAsCoverImage: true,
516
+ postsAsContentImage: true,
517
+ // ---------------------------
518
+ // --- custom fields
519
+ // ---------------------------
520
+ postsAsImages1: true,
521
+ postsAsImages2: true,
522
+ postsAsImage1: true,
523
+ postsAsImage2: true,
524
+ postsAsImage3: true,
525
+ postsAsImage4: true,
526
+ // ---------------------------
527
+ // translation
528
+ // ---------------------------
529
+ translations: true
530
+ };
531
+
532
+ // src/server/infrastructure/database/file/command/create-file-command-repository.ts
533
+ function createFileCommandRepository(prisma) {
534
+ async function create({
535
+ // meta
536
+ fileMeta,
537
+ // ------------------------------------
538
+ // relations
539
+ // ------------------------------------
540
+ // Folder
541
+ folder,
542
+ // ------------------------------------
543
+ // translation
544
+ // ------------------------------------
545
+ translations,
546
+ // rest
547
+ ...params
548
+ }) {
549
+ const extension = mimeToExtension(fileMeta.type);
550
+ const created = await prisma.file.create({
551
+ data: {
552
+ ...params,
553
+ // derived
554
+ originalName: fileMeta.name || "unknown",
555
+ size: fileMeta.size ?? 0,
556
+ extension,
557
+ mimeType: fileMeta.type || "unknown",
558
+ type: classifyFileType(fileMeta.type, extension),
559
+ // ------------------------------------------------------------------------
560
+ // relations
561
+ // ------------------------------------------------------------------------
562
+ // Folder
563
+ folder: connectOne(folder),
564
+ // ------------------------------------------------------------------------
565
+ // translation
566
+ // ------------------------------------------------------------------------
567
+ translations: {
568
+ create: translations.map((t) => ({
569
+ ...t,
570
+ name: t.name ?? fileMeta.name ?? "unknown"
571
+ }))
572
+ }
573
+ },
574
+ include: FILE_FULL_INCLUDE
575
+ });
576
+ return created;
577
+ }
578
+ async function update({
579
+ file,
580
+ // Original file (File)
581
+ id,
582
+ // meta
583
+ fileMeta,
584
+ // ------------------------------------
585
+ // relations
586
+ // ------------------------------------
587
+ // Folder
588
+ folder,
589
+ // ------------------------------------
590
+ // translation
591
+ // ------------------------------------
592
+ translations,
593
+ // rest
594
+ ...params
595
+ }) {
596
+ const extension = mimeToExtension(fileMeta.type);
597
+ const updated = await prisma.file.update({
598
+ where: { id: file.id },
599
+ data: {
600
+ ...params,
601
+ // derived
602
+ size: fileMeta.size ?? file.size,
603
+ extension: fileMeta ? extension : file.extension,
604
+ mimeType: fileMeta.type || file.mimeType,
605
+ type: fileMeta.type ? classifyFileType(fileMeta.type, extension) : file.type,
606
+ // ------------------------------------------------------------------------
607
+ // relations
608
+ // ------------------------------------------------------------------------
609
+ // Folder
610
+ folder: updateOne(folder),
611
+ // ------------------------------------------------------------------------
612
+ // translation
613
+ // ------------------------------------------------------------------------
614
+ translations: {
615
+ upsert: translations.map((t) => ({
616
+ where: { fileId_locale: { fileId: id, locale: t.locale } },
617
+ update: t,
618
+ create: t
619
+ }))
620
+ }
621
+ }
622
+ });
623
+ return updated;
624
+ }
625
+ async function softDelete({ id }) {
626
+ const updated = await prisma.file.update({
627
+ where: { id, isLocked: false },
628
+ data: { deletedAt: /* @__PURE__ */ new Date() }
629
+ });
630
+ return updated;
631
+ }
632
+ async function softDeleteMany({ ids: ids2 }) {
633
+ const { count } = await prisma.file.updateMany({
634
+ where: { id: { in: ids2 }, isLocked: false },
635
+ data: { deletedAt: /* @__PURE__ */ new Date() }
636
+ });
637
+ return count;
638
+ }
639
+ async function restoreMany({ ids: ids2 }) {
640
+ const { count } = await prisma.file.updateMany({
641
+ where: { id: { in: ids2 } },
642
+ data: { deletedAt: null }
643
+ });
644
+ return count;
645
+ }
646
+ async function _delete({ id }) {
647
+ const deleted = await prisma.file.delete({
648
+ where: { id }
649
+ });
650
+ return deleted;
651
+ }
652
+ return {
653
+ create,
654
+ update,
655
+ softDelete,
656
+ softDeleteMany,
657
+ restoreMany,
658
+ delete: _delete
659
+ };
660
+ }
661
+
662
+ // src/server/infrastructure/database/utils/build-file-usage.ts
663
+ function buildFileUsage(isLocked) {
664
+ const relations = [
665
+ "adminAsAvatarImage",
666
+ "postsAsCoverImage",
667
+ "postsAsContentImage",
668
+ "postsAsImages1",
669
+ "postsAsImages2",
670
+ "postsAsImage1",
671
+ "postsAsImage2",
672
+ "postsAsImage3",
673
+ "postsAsImage4"
674
+ ];
675
+ if (isLocked === void 0 || isLocked === null) return {};
676
+ const condition = relations.map((r) => ({
677
+ [r]: { [isLocked ? "some" : "none"]: {} }
678
+ }));
679
+ return isLocked ? { OR: condition } : { AND: condition };
680
+ }
681
+
682
+ // src/server/infrastructure/database/file/query/create-file-query-repository.ts
683
+ function createFileQueryRepository(prisma) {
684
+ async function findListCards({
685
+ locale,
686
+ // pagination
687
+ page,
688
+ pageSize,
689
+ // search
690
+ searchString,
691
+ type,
692
+ folderId,
693
+ isLocked,
694
+ isDeleted = false,
695
+ fileIds
696
+ }) {
697
+ const where = {
698
+ // search
699
+ ...createSearch({
700
+ ...searchString !== void 0 ? { searchString } : {},
701
+ locale,
702
+ translationFields: ["name"]
703
+ }),
704
+ // type
705
+ ...type ? { type } : {},
706
+ // state: isLocked
707
+ ...buildFileUsage(isLocked),
708
+ // state: deleteAt
709
+ deletedAt: isDeleted ? { not: null } : null,
710
+ // Find deleted files in trash page
711
+ // relations: Folder
712
+ ...folderId ? { folderId: folderId === ROOT_FOLDER_ID ? null : folderId } : {},
713
+ // Specified ids
714
+ ...fileIds ? { id: { in: fileIds } } : {}
715
+ };
716
+ const [items, total] = await prisma.$transaction([
717
+ prisma.file.findMany({
718
+ where,
719
+ orderBy: [...ORDER_BY],
720
+ include: FILE_FULL_INCLUDE,
721
+ ...createPagination(page, pageSize)
722
+ }),
723
+ prisma.file.count({ where })
724
+ ]);
725
+ return { items, total };
726
+ }
727
+ async function findManyByIds({
728
+ ids: ids2
729
+ }) {
730
+ return await prisma.file.findMany({
731
+ where: { id: { in: ids2 } },
732
+ include: FILE_FULL_INCLUDE
733
+ });
734
+ }
735
+ async function findFull({ id }) {
736
+ return await prisma.file.findUnique({
737
+ where: { id },
738
+ include: FILE_FULL_INCLUDE
739
+ });
740
+ }
741
+ return {
742
+ findListCards,
743
+ findManyByIds,
744
+ findFull
745
+ };
746
+ }
747
+
748
+ // src/server/infrastructure/database/folder/command/create-folder-command-repository.ts
749
+ function createFolderCommandRepository(prisma) {
750
+ async function create({
751
+ // ------------------------------------
752
+ // relations
753
+ // ------------------------------------
754
+ // Folder
755
+ parentFolder,
756
+ // rest
757
+ ...params
758
+ }) {
759
+ const created = await prisma.folder.create({
760
+ data: {
761
+ ...params,
762
+ // ------------------------------------------------------------------------
763
+ // relations
764
+ // ------------------------------------------------------------------------
765
+ // Folder
766
+ parentFolder: connectOne(parentFolder)
767
+ }
768
+ });
769
+ return created;
770
+ }
771
+ async function update({
772
+ id,
773
+ // ------------------------------------
774
+ // relations
775
+ // ------------------------------------
776
+ // Folder
777
+ parentFolder,
778
+ // rest
779
+ ...params
780
+ }) {
781
+ const updated = await prisma.folder.update({
782
+ where: { id },
783
+ data: {
784
+ ...params,
785
+ // ------------------------------------------------------------------------
786
+ // relations
787
+ // ------------------------------------------------------------------------
788
+ // Folder
789
+ parentFolder: updateOne(parentFolder)
790
+ }
791
+ });
792
+ return updated;
793
+ }
794
+ async function _delete({ id }) {
795
+ const deleted = await prisma.folder.delete({
796
+ where: { id }
797
+ });
798
+ return deleted;
799
+ }
800
+ return {
801
+ create,
802
+ update,
803
+ delete: _delete
804
+ };
805
+ }
806
+
807
+ // src/server/infrastructure/database/folder/include.ts
808
+ var FOLDER_FULL_INCLUDE = {
809
+ // ---------------------------
810
+ // relations: Folder
811
+ // ---------------------------
812
+ parentFolder: { include: { subFolders: true, files: true } },
813
+ subFolders: true,
814
+ // ---------------------------
815
+ // relations: File
816
+ // ---------------------------
817
+ files: true
818
+ };
819
+
820
+ // src/server/infrastructure/database/folder/query/create-folder-query-repository.ts
821
+ function buildWhere2(params) {
822
+ if (params.id) return { id: params.id };
823
+ if (params.key) return { key: params.key };
824
+ return;
825
+ }
826
+ function createFolderQueryRepository(prisma) {
827
+ async function findListCards({
828
+ // search
829
+ searchString,
830
+ parentFolderId,
831
+ folderIds,
832
+ // pagination
833
+ page,
834
+ pageSize
835
+ }) {
836
+ const where = {
837
+ // search
838
+ ...searchString ? { name: { contains: searchString, mode: "insensitive" } } : {},
839
+ // relations: Folder
840
+ ...parentFolderId ? parentFolderId === ROOT_FOLDER_ID ? { parentFolderId: null } : { parentFolderId } : {},
841
+ // Specified ids
842
+ ...folderIds ? { id: { in: folderIds } } : {}
843
+ };
844
+ const [items, total] = await prisma.$transaction([
845
+ prisma.folder.findMany({
846
+ where,
847
+ orderBy: [...ORDER_BY],
848
+ include: FOLDER_FULL_INCLUDE,
849
+ ...createPagination(page, pageSize)
850
+ }),
851
+ prisma.folder.count({ where })
852
+ ]);
853
+ return { items, total };
854
+ }
855
+ async function findFull(params) {
856
+ const where = buildWhere2(params);
857
+ if (!where) return null;
858
+ return await prisma.folder.findUnique({
859
+ where,
860
+ include: FOLDER_FULL_INCLUDE
861
+ });
862
+ }
863
+ return {
864
+ findListCards,
865
+ findFull
866
+ };
867
+ }
868
+ function createPostCommandRepository(prisma) {
869
+ async function create({
870
+ slug,
871
+ // ------------------------------------
872
+ // relations
873
+ // ------------------------------------
874
+ // Admin
875
+ author,
876
+ // Post
877
+ topicId,
878
+ parents,
879
+ tags,
880
+ relatedPosts,
881
+ // File
882
+ coverImage,
883
+ contentImageIds,
884
+ // ------------------------------------
885
+ // --- custom fields
886
+ // ------------------------------------
887
+ // multi images
888
+ images1,
889
+ images2,
890
+ // single images
891
+ image1,
892
+ image2,
893
+ image3,
894
+ image4,
895
+ // ------------------------------------
896
+ // translation
897
+ // ------------------------------------
898
+ translations,
899
+ // rest
900
+ ...params
901
+ }) {
902
+ const id = ulid();
903
+ const created = await prisma.post.create({
904
+ data: {
905
+ ...params,
906
+ id,
907
+ // ------------------------------------------------------------------------
908
+ // states
909
+ // ------------------------------------------------------------------------
910
+ slug: slug?.trim() || id,
911
+ // Use id when slug is null or empty string
912
+ // ------------------------------------------------------------------------
913
+ // relations
914
+ // ------------------------------------------------------------------------
915
+ // Admin
916
+ author: connectOne(author),
917
+ // Post
918
+ topic: connectOne(topicId ? { id: topicId } : null),
919
+ parents: connectMany(parents),
920
+ tags: connectMany(tags),
921
+ relatedPosts: connectMany(relatedPosts),
922
+ // File
923
+ coverImage: connectOne(coverImage),
924
+ contentImages: connectMany(contentImageIds.map((id2) => ({ id: id2 }))),
925
+ // ------------------------------------------------------------------------
926
+ // --- custom fields
927
+ // ------------------------------------------------------------------------
928
+ // multi images
929
+ images1: connectMany(images1),
930
+ images2: connectMany(images2),
931
+ // single images
932
+ image1: connectOne(image1),
933
+ image2: connectOne(image2),
934
+ image3: connectOne(image3),
935
+ image4: connectOne(image4),
936
+ // ------------------------------------------------------------------------
937
+ // translation
938
+ // ------------------------------------------------------------------------
939
+ translations: {
940
+ create: translations.map((t) => ({
941
+ ...t,
942
+ externalLinks: t.externalLinks,
943
+ faq: t.faq,
944
+ toc: t.toc
945
+ }))
946
+ }
947
+ }
948
+ });
949
+ return created;
950
+ }
951
+ async function update({
952
+ id,
953
+ slug,
954
+ // ------------------------------------
955
+ // relations
956
+ // ------------------------------------
957
+ // Admin
958
+ author,
959
+ // Post
960
+ topicId,
961
+ parents,
962
+ tags,
963
+ relatedPosts,
964
+ // File
965
+ coverImage,
966
+ contentImageIds,
967
+ // ------------------------------------
968
+ // --- custom fields
969
+ // ------------------------------------
970
+ // multi images
971
+ images1,
972
+ images2,
973
+ // single images
974
+ image1,
975
+ image2,
976
+ image3,
977
+ image4,
978
+ // ------------------------------------
979
+ // translation
980
+ // ------------------------------------
981
+ translations,
982
+ // rest
983
+ ...params
984
+ }) {
985
+ const updated = await prisma.post.update({
986
+ where: { id },
987
+ data: {
988
+ ...params,
989
+ // ------------------------------------------------------------------------
990
+ // states
991
+ // ------------------------------------------------------------------------
992
+ slug: slug?.trim() || id,
993
+ // Use id when slug is null or empty string
994
+ // ------------------------------------------------------------------------
995
+ // relations
996
+ // ------------------------------------------------------------------------
997
+ // Admin
998
+ author: updateOne(author),
999
+ // Post
1000
+ topic: updateOne(topicId ? { id: topicId } : null),
1001
+ parents: updateMany(parents),
1002
+ tags: updateMany(tags),
1003
+ relatedPosts: updateMany(relatedPosts),
1004
+ // File
1005
+ coverImage: updateOne(coverImage),
1006
+ contentImages: updateMany(contentImageIds.map((id2) => ({ id: id2 }))),
1007
+ // ------------------------------------------------------------------------
1008
+ // --- custom fields
1009
+ // ------------------------------------------------------------------------
1010
+ // multi images
1011
+ images1: updateMany(images1),
1012
+ images2: updateMany(images2),
1013
+ // single images
1014
+ image1: updateOne(image1),
1015
+ image2: updateOne(image2),
1016
+ image3: updateOne(image3),
1017
+ image4: updateOne(image4),
1018
+ // ------------------------------------------------------------------------
1019
+ // translation
1020
+ // ------------------------------------------------------------------------
1021
+ translations: {
1022
+ upsert: translations.map((t) => ({
1023
+ where: { postId_locale: { postId: id, locale: t.locale } },
1024
+ update: {
1025
+ ...t,
1026
+ externalLinks: t.externalLinks,
1027
+ faq: t.faq,
1028
+ toc: t.toc
1029
+ },
1030
+ create: {
1031
+ ...t,
1032
+ externalLinks: t.externalLinks,
1033
+ faq: t.faq,
1034
+ toc: t.toc
1035
+ }
1036
+ }))
1037
+ }
1038
+ }
1039
+ });
1040
+ return updated;
1041
+ }
1042
+ async function _delete({ id }) {
1043
+ const deleted = await prisma.post.delete({
1044
+ where: { id }
1045
+ });
1046
+ return deleted;
1047
+ }
1048
+ return {
1049
+ create,
1050
+ update,
1051
+ delete: _delete
1052
+ };
1053
+ }
1054
+
1055
+ // src/server/infrastructure/database/post/include.ts
1056
+ var POST_LIST_CARD_INCLUDE = {
1057
+ // ---------------------------
1058
+ // relations: Post
1059
+ // ---------------------------
1060
+ topic: { include: { translations: true } },
1061
+ // Use as post, category
1062
+ postsInTopic: true,
1063
+ // Use as topic
1064
+ children: true,
1065
+ // Use as category
1066
+ taggedPosts: true,
1067
+ // Use as tag
1068
+ // ---------------------------
1069
+ // relations: File
1070
+ // ---------------------------
1071
+ coverImage: true,
1072
+ // ---------------------------
1073
+ // translation
1074
+ // ---------------------------
1075
+ translations: true
1076
+ };
1077
+ var POST_FULL_INCLUDE = {
1078
+ // ---------------------------
1079
+ // relations: Admin
1080
+ // ---------------------------
1081
+ author: { include: { translations: true } },
1082
+ // ---------------------------
1083
+ // relations: Post
1084
+ // ---------------------------
1085
+ topic: { include: POST_LIST_CARD_INCLUDE },
1086
+ postsInTopic: { include: POST_LIST_CARD_INCLUDE },
1087
+ parents: { include: POST_LIST_CARD_INCLUDE },
1088
+ children: { include: POST_LIST_CARD_INCLUDE },
1089
+ tags: { include: POST_LIST_CARD_INCLUDE },
1090
+ taggedPosts: { include: POST_LIST_CARD_INCLUDE },
1091
+ relatedPosts: { include: POST_LIST_CARD_INCLUDE },
1092
+ referencingPosts: { include: POST_LIST_CARD_INCLUDE },
1093
+ // ---------------------------
1094
+ // relations: File
1095
+ // ---------------------------
1096
+ coverImage: { include: { translations: true } },
1097
+ // ---------------------------
1098
+ // --- custom fields
1099
+ // ---------------------------
1100
+ images1: { include: { translations: true } },
1101
+ images2: { include: { translations: true } },
1102
+ image1: { include: { translations: true } },
1103
+ image2: { include: { translations: true } },
1104
+ image3: { include: { translations: true } },
1105
+ image4: { include: { translations: true } },
1106
+ // ---------------------------
1107
+ // translation
1108
+ // ---------------------------
1109
+ translations: true
1110
+ };
1111
+
1112
+ // src/server/infrastructure/database/post/query/create-post-query-repository.ts
1113
+ function buildWhere3(params) {
1114
+ if (params.id) return { id: params.id };
1115
+ if (params.type && params.slug)
1116
+ return { type: params.type, slug: params.slug };
1117
+ return;
1118
+ }
1119
+ function createPostQueryRepository(prisma) {
1120
+ async function findListCards({
1121
+ locale,
1122
+ // search
1123
+ searchString,
1124
+ type,
1125
+ topicId,
1126
+ postIds,
1127
+ isActive,
1128
+ isIndexActive,
1129
+ isSlugActive,
1130
+ isFeatured,
1131
+ isShownOnHome,
1132
+ // pagination
1133
+ page,
1134
+ pageSize
1135
+ }) {
1136
+ const where = {
1137
+ // search
1138
+ ...createSearch({
1139
+ ...searchString ? { searchString } : {},
1140
+ locale,
1141
+ rootFields: ["slug"],
1142
+ translationFields: ["title", "summary", "description", "content"]
1143
+ }),
1144
+ // type
1145
+ ...type ? { type } : {},
1146
+ // states
1147
+ ...isActive ? { isActive: true } : {},
1148
+ ...isIndexActive ? { isIndexActive: true } : {},
1149
+ ...isSlugActive ? { isSlugActive: true } : {},
1150
+ ...isFeatured ? { isFeatured: true } : {},
1151
+ ...isShownOnHome ? { isShownOnHome: true } : {},
1152
+ // relations: Post
1153
+ ...topicId ? { topicId } : {},
1154
+ // Specified ids
1155
+ ...postIds ? { id: { in: postIds } } : {}
1156
+ };
1157
+ const [items, total] = await prisma.$transaction([
1158
+ prisma.post.findMany({
1159
+ where,
1160
+ orderBy: POST_ORDER_BY,
1161
+ include: POST_LIST_CARD_INCLUDE,
1162
+ ...createPagination(page, pageSize)
1163
+ }),
1164
+ prisma.post.count({ where })
1165
+ ]);
1166
+ return { items, total };
1167
+ }
1168
+ async function find(params) {
1169
+ const where = buildWhere3(params);
1170
+ if (!where) return null;
1171
+ return prisma.post.findFirst({
1172
+ where,
1173
+ include: { translations: true }
1174
+ });
1175
+ }
1176
+ async function findMany({
1177
+ type,
1178
+ topicId
1179
+ }) {
1180
+ return await prisma.post.findMany({
1181
+ where: { type, ...topicId ? { topicId } : {} },
1182
+ orderBy: POST_ORDER_BY,
1183
+ include: { translations: true }
1184
+ });
1185
+ }
1186
+ async function findFull(params) {
1187
+ const where = buildWhere3(params);
1188
+ if (!where) return null;
1189
+ return prisma.post.findFirst({
1190
+ where,
1191
+ include: POST_FULL_INCLUDE
1192
+ });
1193
+ }
1194
+ async function findWithSeoMetadata(params) {
1195
+ const where = buildWhere3(params);
1196
+ if (!where) return null;
1197
+ return await prisma.post.findFirst({
1198
+ where,
1199
+ include: { translations: true, seoMetadatas: true }
1200
+ });
1201
+ }
1202
+ return {
1203
+ findListCards,
1204
+ findMany,
1205
+ find,
1206
+ findFull,
1207
+ findWithSeoMetadata
1208
+ };
1209
+ }
1210
+
1211
+ // src/server/infrastructure/database/seo-metadata/command/create-seo-metadata-command-repository.ts
1212
+ function createSeoMetadataCommandRepository(prisma) {
1213
+ async function upsert({
1214
+ // ------------------------------------
1215
+ // relations
1216
+ // ------------------------------------
1217
+ // Post
1218
+ postId,
1219
+ // ------------------------------------
1220
+ // translation
1221
+ // ------------------------------------
1222
+ translations
1223
+ }) {
1224
+ await prisma.post.update({
1225
+ where: { id: postId },
1226
+ data: {
1227
+ seoMetadatas: {
1228
+ upsert: translations.map((t) => ({
1229
+ where: { postId_locale: { postId, locale: t.locale } },
1230
+ update: t,
1231
+ create: t
1232
+ }))
1233
+ }
1234
+ }
1235
+ });
1236
+ }
1237
+ return {
1238
+ upsert
1239
+ };
1240
+ }
1241
+
1242
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };