@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.
- package/dist/{chunk-KEQXXUK2.js → chunk-SEX4DOKX.js} +27 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +1 -1
- package/dist/server/index.d.ts +28 -2
- package/dist/server/index.js +122 -5
- package/package.json +4 -1
|
@@ -204,4 +204,30 @@ var formatFileSize = (size, decimals = 2) => {
|
|
|
204
204
|
return `${display} ${units[unitIndex]}`;
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
|
|
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
|
-
|
|
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-
|
|
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';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { ADMIN_ROLES, mimeToExtension, classifyFileType, ROOT_FOLDER_ID } from '../chunk-
|
|
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
|
|
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: {
|
|
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.
|
|
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
|
},
|