@yimingliao/cms 0.0.13 → 0.0.15

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.
@@ -204,4 +204,30 @@ var formatFileSize = (size, decimals = 2) => {
204
204
  return `${display} ${units[unitIndex]}`;
205
205
  };
206
206
 
207
- export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension };
207
+ // src/shared/result/result.ts
208
+ function success({
209
+ message,
210
+ data,
211
+ meta
212
+ }) {
213
+ return {
214
+ success: true,
215
+ ...message !== void 0 ? { message } : {},
216
+ ...data !== void 0 ? { data } : {},
217
+ ...meta !== void 0 ? { meta } : {}
218
+ };
219
+ }
220
+ function error({ message, errors, code }) {
221
+ return {
222
+ success: false,
223
+ ...message !== void 0 ? { message } : {},
224
+ ...errors !== void 0 ? { errors } : {},
225
+ ...code !== void 0 ? { code } : {}
226
+ };
227
+ }
228
+ var result = {
229
+ success,
230
+ error
231
+ };
232
+
233
+ export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result };
package/dist/index.d.ts CHANGED
@@ -27,4 +27,44 @@ interface BlobFile extends File {
27
27
  duration: number | null;
28
28
  }
29
29
 
30
- export { type BlobFile, FolderFull, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension };
30
+ interface SuccessResult<D = unknown> {
31
+ success: true;
32
+ message?: string;
33
+ data?: D;
34
+ meta?: Record<string, unknown>;
35
+ }
36
+ interface ErrorResult {
37
+ success: false;
38
+ message?: string;
39
+ errors?: ErrorDetail[];
40
+ code?: string;
41
+ }
42
+ interface ErrorDetail {
43
+ field?: string;
44
+ message?: string;
45
+ code?: string;
46
+ }
47
+ type Result<D = unknown> = SuccessResult<D> | ErrorResult;
48
+
49
+ interface SuccessResultParams<D = unknown> {
50
+ message?: string;
51
+ data?: D;
52
+ meta?: Record<string, unknown>;
53
+ }
54
+ declare function success<D = unknown>({ message, data, meta, }: SuccessResultParams<D>): SuccessResult<D>;
55
+ interface ErrorResultParams {
56
+ message?: string;
57
+ errors?: ErrorDetail[];
58
+ code?: string;
59
+ }
60
+ declare function error({ message, errors, code }: ErrorResultParams): ErrorResult;
61
+ /**
62
+ * Generic result interface for API and action responses.
63
+ * Provides a unified structure for success and error outputs.
64
+ */
65
+ declare const result: {
66
+ success: typeof success;
67
+ error: typeof error;
68
+ };
69
+
70
+ export { type BlobFile, type ErrorDetail, type ErrorResult, type ErrorResultParams, FolderFull, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, type Result, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, type SuccessResult, type SuccessResultParams, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension } from './chunk-KEQXXUK2.js';
1
+ export { ADMIN_ROLES, FILE_TYPES, POST_TYPES, ROOT_FOLDER, ROOT_FOLDER_ID, ROOT_FOLDER_NAME, SIMPLE_UPLOAD_FOLDER_KEY, SIMPLE_UPLOAD_FOLDER_NAME, classifyFileType, formatFileSize, getMediaInfo, mimeToExtension, result } from './chunk-SEX4DOKX.js';
@@ -1,6 +1,8 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
  import { BinaryLike } from 'node:crypto';
3
3
  import { cookies } from 'next/headers';
4
+ import Keyv from 'keyv';
5
+ import { Logger } from 'logry';
4
6
  import { e as AdminRole, v as SingleItem, B as BaseTranslation, a as Admin, c as AdminFull, f as AdminSafe, D as DeviceInfo, d as AdminRefreshToken, k as File, m as FileFull, o as FileType, p as Folder, F as FolderFull, u as PostType, M as MultiItems, E as ExternalLink, j as Faq, T as TocItem, q as Post, s as PostListCard, t as PostTranslation, r as PostFull, S as SeoMetadata, g as AdminTranslation, n as FileTranslation, h as Alternate } from '../base-DbGnfZr6.js';
5
7
 
6
8
  interface JwtServiceConfig {
@@ -84,6 +86,29 @@ declare function createCookieService(nextCookies: () => Promise<Awaited<ReturnTy
84
86
  }) => Promise<void>;
85
87
  };
86
88
 
89
+ interface CreateServerCacheOptions {
90
+ redisUrl: string;
91
+ namespace: string;
92
+ keyDelimiter: string;
93
+ }
94
+ declare function createCache({ redisUrl, namespace, keyDelimiter, }: CreateServerCacheOptions): Keyv<unknown>;
95
+
96
+ type RawCacheKey = string | Array<string | number | boolean | undefined | null>;
97
+
98
+ interface CacheResultOptions<T> {
99
+ key: RawCacheKey;
100
+ ttl?: number;
101
+ load: () => Promise<T>;
102
+ }
103
+ declare function createCacheResult(cache: Keyv<unknown>, logger: Logger): <T>({ key: rawKey, ttl, load, }: CacheResultOptions<T>) => Promise<T>;
104
+
105
+ interface RateLimiterOptions {
106
+ key: RawCacheKey;
107
+ maxAttempts?: number;
108
+ timeWindow?: number;
109
+ }
110
+ declare function createIpRateLimiter(cache: Keyv<unknown>, appName: string): ({ key: rawKey, maxAttempts, timeWindow, }: RateLimiterOptions) => Promise<boolean>;
111
+
87
112
  interface CreateParams$4 {
88
113
  email: string;
89
114
  role: AdminRole;
@@ -491,6 +516,7 @@ interface FindParams {
491
516
  type?: PostType;
492
517
  slug?: string;
493
518
  isActive?: boolean;
519
+ topicSlug?: string;
494
520
  }
495
521
  interface FindManyParams {
496
522
  type: PostType;
@@ -508,7 +534,7 @@ declare function createPostQueryRepository(prisma: any): {
508
534
  find: ({ isActive, ...rest }: FindParams) => Promise<(Post & {
509
535
  translations: PostTranslation[];
510
536
  }) | null>;
511
- findFull: ({ isActive, ...rest }: FindParams) => Promise<PostFull | null>;
537
+ findFull: ({ isActive, topicSlug, ...rest }: FindParams) => Promise<PostFull | null>;
512
538
  findWithSeoMetadata: ({ isActive, ...rest }: FindParams) => Promise<(Post & {
513
539
  translations: PostTranslation[];
514
540
  seoMetadatas: SeoMetadata[];
@@ -584,4 +610,4 @@ declare const POST_ORDER_BY: ({
584
610
  index: "asc";
585
611
  })[];
586
612
 
587
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
613
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
@@ -1,8 +1,10 @@
1
- import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-KEQXXUK2.js';
1
+ import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-SEX4DOKX.js';
2
2
  import jwt from 'jsonwebtoken';
3
3
  import argon2, { argon2id } from 'argon2';
4
4
  import crypto, { timingSafeEqual } from 'crypto';
5
- import 'next/headers';
5
+ import { headers } from 'next/headers';
6
+ import KeyvRedis from '@keyv/redis';
7
+ import Keyv from 'keyv';
6
8
  import { ulid } from 'ulid';
7
9
 
8
10
  function createJwtService(config) {
@@ -193,6 +195,112 @@ function createCookieService(nextCookies, cryptoService) {
193
195
  delete: deleteCokkie
194
196
  };
195
197
  }
198
+ function createCache({
199
+ redisUrl,
200
+ namespace,
201
+ keyDelimiter
202
+ }) {
203
+ const redisStore = new KeyvRedis(redisUrl, {
204
+ keyPrefixSeparator: keyDelimiter
205
+ });
206
+ return new Keyv({
207
+ store: redisStore,
208
+ namespace,
209
+ useKeyPrefix: false
210
+ });
211
+ }
212
+
213
+ // src/server/infrastructure/cache/cache-key-delimiter.ts
214
+ var CACHE_KEY_DELIMITER = "|";
215
+
216
+ // src/server/infrastructure/cache/normalize-cache-key.ts
217
+ var sanitize = (k) => k.replaceAll(/[\u200B-\u200D\uFEFF]/g, "").replaceAll(/[\r\n]/g, "").trim();
218
+ var normalizeCacheKey = (key, delimiter = CACHE_KEY_DELIMITER) => {
219
+ if (!key) return null;
220
+ if (Array.isArray(key)) {
221
+ if (key.length === 0) return null;
222
+ const normalized = key.map((k) => {
223
+ if (k === null) return "__null";
224
+ if (k === void 0) return "__undefined";
225
+ if (typeof k === "boolean") return k ? "__true" : "__false";
226
+ return sanitize(String(k));
227
+ });
228
+ return normalized.join(delimiter);
229
+ }
230
+ if (typeof key === "boolean") return key ? "__true" : "__false";
231
+ return String(key);
232
+ };
233
+
234
+ // src/server/infrastructure/cache/create-cache-result.ts
235
+ var DAY = 1e3 * 60 * 60 * 24;
236
+ function createCacheResult(cache, logger) {
237
+ return async function cacheResult({
238
+ key: rawKey,
239
+ ttl = DAY,
240
+ load
241
+ }) {
242
+ const key = normalizeCacheKey(rawKey);
243
+ if (!key) {
244
+ logger.error("Cache skipped due to invalid key", {
245
+ rawKey,
246
+ scope: "cacheResult"
247
+ });
248
+ return load();
249
+ }
250
+ try {
251
+ const cached = await cache.get(key);
252
+ if (cached !== void 0) {
253
+ return cached;
254
+ }
255
+ const data = await load();
256
+ if (data !== void 0) {
257
+ await cache.set(key, data, ttl);
258
+ }
259
+ return data;
260
+ } catch (error) {
261
+ logger.error("Cache failure, falling back to loader", {
262
+ key,
263
+ error,
264
+ scope: "cacheResult"
265
+ });
266
+ return load();
267
+ }
268
+ };
269
+ }
270
+ var DEFAULT_MAX_ATTEMPTS = 10;
271
+ var DEFAULT_TIME_WINDOW = 60;
272
+ var lua = `
273
+ local current = redis.call('INCR', KEYS[1])
274
+ if current == 1 then
275
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
276
+ end
277
+ return current
278
+ `;
279
+ function createIpRateLimiter(cache, appName) {
280
+ return async function ipRateLimiter({
281
+ key: rawKey,
282
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
283
+ timeWindow = DEFAULT_TIME_WINDOW
284
+ }) {
285
+ const secondaryStore = cache.store;
286
+ const redis = await secondaryStore.getClient();
287
+ if (!redis) return true;
288
+ const headersStore = await headers();
289
+ const ip = headersStore.get("cf-connecting-ip")?.trim() || headersStore.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
290
+ const key = normalizeCacheKey([
291
+ appName,
292
+ "ip-rate-limiter",
293
+ ...Array.isArray(rawKey) ? rawKey : [rawKey],
294
+ ip
295
+ ]);
296
+ const currentRaw = await redis.eval(lua, {
297
+ keys: [key],
298
+ arguments: [timeWindow.toString()]
299
+ });
300
+ const current = typeof currentRaw === "number" ? currentRaw : Number(currentRaw);
301
+ return current <= maxAttempts;
302
+ };
303
+ }
196
304
 
197
305
  // src/server/infrastructure/database/utils/connect.ts
198
306
  var ids = (items) => items.map(({ id }) => ({ id }));
@@ -1183,7 +1291,7 @@ function createPostQueryRepository(prisma) {
1183
1291
  ...state8 ? { state8: true } : {},
1184
1292
  ...state9 ? { state9: true } : {},
1185
1293
  ...state10 ? { state10: true } : {},
1186
- // relations: Post
1294
+ // relations
1187
1295
  ...topicId ? { topicId } : {},
1188
1296
  ...topicSlug ? { topic: { slug: topicSlug } } : {},
1189
1297
  ...categoryId ? { parents: { some: { id: categoryId } } } : {},
@@ -1229,13 +1337,22 @@ function createPostQueryRepository(prisma) {
1229
1337
  });
1230
1338
  }
1231
1339
  async function findFull({
1340
+ // states
1232
1341
  isActive,
1342
+ // relations
1343
+ topicSlug,
1233
1344
  ...rest
1234
1345
  }) {
1235
1346
  const where = buildWhere3(rest);
1236
1347
  if (!where) return null;
1237
1348
  return prisma.post.findFirst({
1238
- where: { ...where, ...isActive ? { isActive } : {} },
1349
+ where: {
1350
+ ...where,
1351
+ // states
1352
+ ...isActive ? { isActive } : {},
1353
+ // relations
1354
+ ...topicSlug ? { topic: { slug: topicSlug } } : {}
1355
+ },
1239
1356
  include: POST_FULL_INCLUDE
1240
1357
  });
1241
1358
  }
@@ -1298,4 +1415,4 @@ function createSeoMetadataCommandRepository(prisma) {
1298
1415
  };
1299
1416
  }
1300
1417
 
1301
- export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
1418
+ export { ADMIN_ORDER_BY, ORDER_BY, POST_ORDER_BY, createAdminCommandRepository, createAdminQueryRepository, createAdminRefreshTokenCommandRepository, createAdminRefreshTokenQueryRepository, createArgon2Service, createCache, createCacheResult, createCookieService, createCryptoService, createFileCommandRepository, createFileQueryRepository, createFolderCommandRepository, createFolderQueryRepository, createIpRateLimiter, createJwtService, createPostCommandRepository, createPostQueryRepository, createSeoMetadataCommandRepository };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yimingliao/cms",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "author": "Yiming Liao",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,8 +26,11 @@
26
26
  "prepublishOnly": "yarn build"
27
27
  },
28
28
  "dependencies": {
29
+ "@keyv/redis": "^5.1.6",
29
30
  "argon2": "^0.44.0",
30
31
  "jsonwebtoken": "^9.0.3",
32
+ "keyv": "^5.6.0",
33
+ "logry": "^2.1.6",
31
34
  "mime-types": "^3.0.2",
32
35
  "ulid": "^3.0.2"
33
36
  },