@ai0x0/utils 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -0
- package/es/backend/actions/write-action-core.js +37 -17
- package/es/backend/errors.d.ts +30 -0
- package/es/backend/errors.js +80 -1
- package/es/backend/route-operation/operation-common.js +49 -18
- package/lib/backend/actions/write-action-core.js +4 -4
- package/lib/backend/errors.d.ts +30 -0
- package/lib/backend/errors.js +29 -2
- package/lib/backend/route-operation/operation-common.js +7 -4
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -317,6 +317,8 @@ export const { GET } = route({
|
|
|
317
317
|
- **Fail closed**: if the session is missing → 401; if `scope.value` returns empty → 403. A scope that cannot be resolved is **never** downgraded to "no filter". PUT/DELETE put the scope straight into the `UPDATE`/`DELETE` `where` (one statement, no read-then-write window) and return 404 when it affects zero rows — "does not exist" and "not yours" are deliberately indistinguishable, otherwise the endpoint becomes an existence oracle.
|
|
318
318
|
- **Auto injection**: POST stamps the scope column with the scope value (by default that is `creatorId = session.userId`); PUT writes `editorId = session.userId`; GET/GET_LIST apply the scope as a mandatory `where`.
|
|
319
319
|
- **Session shape** is yours: `getSession` is generic, so resolve "who + which space + what role" once and let `scope.value` / `can` read the same object instead of each querying again.
|
|
320
|
+
- **Built-in error copy** (`未登录` / `没有权限` / `缺少 id` / `未找到编辑(删除)对象,或没有权限` / scope-unresolved 403): thrown from inside the library, so the app's own i18n throw-sites never see them. Register `setHttpErrorMessageResolver(() => ({ unauthenticated: t("..."), ... }))` once at app startup to translate them per request — the resolver may be `async` (e.g. resolving the locale from `next/headers`), is called lazily at each throw site, and falls back to the Chinese defaults when absent or when it throws. Deliberately string-based, not error-code-based: CLI and agent consumers read these messages too and have no translation layer.
|
|
321
|
+
|
|
320
322
|
- **Date coercion**: any body field ending in `At` is passed through `dayjs(value).toDate()` before write — accept ISO strings from clients.
|
|
321
323
|
- **List filtering** (all querystring values are strings). **A column is filterable only if it appears in `schemas.response`** — the filter surface is the readable surface, so a list that returns `id / title` cannot be probed with `?secret=…` (unknown or unreadable keys are ignored, not rejected). Anything you want filterable belongs in the response schema.
|
|
322
324
|
- `key=a,b,c` → `IN (...)` when the key is `id` or ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
@@ -26,7 +26,7 @@ function _asyncToGenerator(fn) { return function () { var self = this, args = ar
|
|
|
26
26
|
|
|
27
27
|
import { and, eq } from "drizzle-orm";
|
|
28
28
|
import { NO_SCOPE, scopeCondition } from "../scope.js";
|
|
29
|
-
import { HttpError } from "../errors.js";
|
|
29
|
+
import { HttpError, httpErrorMessages } from "../errors.js";
|
|
30
30
|
import { transformBody } from "./transform-body.js";
|
|
31
31
|
|
|
32
32
|
/** 核心用得到表的哪一部分:只有 id。dialect 各自的 Table 类型不进来。 */
|
|
@@ -49,25 +49,35 @@ export function createPutActionCore(_ref) {
|
|
|
49
49
|
scope = _ref2.scope;
|
|
50
50
|
id = body.id;
|
|
51
51
|
if (id) {
|
|
52
|
-
_context.next =
|
|
52
|
+
_context.next = 8;
|
|
53
53
|
break;
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
case 4:
|
|
55
|
+
_context.t0 = HttpError;
|
|
57
56
|
_context.next = 6;
|
|
58
|
-
return
|
|
57
|
+
return httpErrorMessages();
|
|
59
58
|
case 6:
|
|
59
|
+
_context.t1 = _context.sent.missingId;
|
|
60
|
+
throw new _context.t0(400, _context.t1);
|
|
61
|
+
case 8:
|
|
62
|
+
_context.next = 10;
|
|
63
|
+
return db.update(table).set(transformBody(body)).where(byId(table, id, scope)).returning();
|
|
64
|
+
case 10:
|
|
60
65
|
_yield$db$update$set$ = _context.sent;
|
|
61
66
|
_yield$db$update$set$2 = _slicedToArray(_yield$db$update$set$, 1);
|
|
62
67
|
data = _yield$db$update$set$2[0];
|
|
63
68
|
if (!(!data && scope !== NO_SCOPE)) {
|
|
64
|
-
_context.next =
|
|
69
|
+
_context.next = 19;
|
|
65
70
|
break;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
_context.t2 = HttpError;
|
|
73
|
+
_context.next = 17;
|
|
74
|
+
return httpErrorMessages();
|
|
75
|
+
case 17:
|
|
76
|
+
_context.t3 = _context.sent.editNotFound;
|
|
77
|
+
throw new _context.t2(404, _context.t3);
|
|
78
|
+
case 19:
|
|
69
79
|
return _context.abrupt("return", data);
|
|
70
|
-
case
|
|
80
|
+
case 20:
|
|
71
81
|
case "end":
|
|
72
82
|
return _context.stop();
|
|
73
83
|
}
|
|
@@ -90,23 +100,33 @@ export function createDeleteActionCore(_ref4) {
|
|
|
90
100
|
id = _ref5.id;
|
|
91
101
|
scope = _ref6.scope;
|
|
92
102
|
if (id) {
|
|
93
|
-
_context2.next =
|
|
103
|
+
_context2.next = 8;
|
|
94
104
|
break;
|
|
95
105
|
}
|
|
96
|
-
|
|
97
|
-
case 4:
|
|
106
|
+
_context2.t0 = HttpError;
|
|
98
107
|
_context2.next = 6;
|
|
99
|
-
return
|
|
108
|
+
return httpErrorMessages();
|
|
100
109
|
case 6:
|
|
110
|
+
_context2.t1 = _context2.sent.missingId;
|
|
111
|
+
throw new _context2.t0(400, _context2.t1);
|
|
112
|
+
case 8:
|
|
113
|
+
_context2.next = 10;
|
|
114
|
+
return db.delete(table).where(byId(table, id, scope)).returning();
|
|
115
|
+
case 10:
|
|
101
116
|
rows = _context2.sent;
|
|
102
117
|
if (!(rows.length === 0 && scope !== NO_SCOPE)) {
|
|
103
|
-
_context2.next =
|
|
118
|
+
_context2.next = 17;
|
|
104
119
|
break;
|
|
105
120
|
}
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
_context2.t2 = HttpError;
|
|
122
|
+
_context2.next = 15;
|
|
123
|
+
return httpErrorMessages();
|
|
124
|
+
case 15:
|
|
125
|
+
_context2.t3 = _context2.sent.deleteNotFound;
|
|
126
|
+
throw new _context2.t2(404, _context2.t3);
|
|
127
|
+
case 17:
|
|
108
128
|
return _context2.abrupt("return", rows);
|
|
109
|
-
case
|
|
129
|
+
case 18:
|
|
110
130
|
case "end":
|
|
111
131
|
return _context2.stop();
|
|
112
132
|
}
|
package/es/backend/errors.d.ts
CHANGED
|
@@ -2,3 +2,33 @@ export declare class HttpError extends Error {
|
|
|
2
2
|
status: number;
|
|
3
3
|
constructor(status: number, message: string);
|
|
4
4
|
}
|
|
5
|
+
export interface HttpErrorMessages {
|
|
6
|
+
/** 401:会话不存在。 */
|
|
7
|
+
unauthenticated: string;
|
|
8
|
+
/** 403:access.can 拒绝且没给自定义文案。 */
|
|
9
|
+
forbidden: string;
|
|
10
|
+
/** 400:PUT / DELETE 请求体缺 id。 */
|
|
11
|
+
missingId: string;
|
|
12
|
+
/** 404:PUT 影响 0 行(不存在或不属于调用者)。 */
|
|
13
|
+
editNotFound: string;
|
|
14
|
+
/** 404:DELETE 影响 0 行。 */
|
|
15
|
+
deleteNotFound: string;
|
|
16
|
+
/** 403:作用域值解析不出来 —— 多半是配错了列名,但请求照样要被拒绝。 */
|
|
17
|
+
scopeUnresolved: (column: string) => string;
|
|
18
|
+
}
|
|
19
|
+
export declare const defaultHttpErrorMessages: HttpErrorMessages;
|
|
20
|
+
type HttpErrorMessageResolver = () => Partial<HttpErrorMessages> | Promise<Partial<HttpErrorMessages>>;
|
|
21
|
+
/**
|
|
22
|
+
* 注册文案解析器,应用启动时调一次(再次调用覆盖;传 undefined 恢复默认)。
|
|
23
|
+
* 不注册就一直是 defaultHttpErrorMessages 那几句中文 —— 没做国际化的项目零感知。
|
|
24
|
+
*/
|
|
25
|
+
export declare function setHttpErrorMessageResolver(next: HttpErrorMessageResolver | undefined): void;
|
|
26
|
+
/**
|
|
27
|
+
* 取出当前生效的整份文案。**在抛出点现调**,别在模块顶层缓存 —— 解析器是按请求判
|
|
28
|
+
* 语言的,缓存住就把上一个请求的语言漏给下一个。
|
|
29
|
+
*
|
|
30
|
+
* 解析器自己抛了错(比如语言包没加载到)就回落默认中文:报错文案出不来不该把
|
|
31
|
+
* 原本那个 401/403 也吞掉。
|
|
32
|
+
*/
|
|
33
|
+
export declare function httpErrorMessages(): Promise<HttpErrorMessages>;
|
|
34
|
+
export {};
|
package/es/backend/errors.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return e; }; var t, e = {}, r = Object.prototype, n = r.hasOwnProperty, o = Object.defineProperty || function (t, e, r) { t[e] = r.value; }, i = "function" == typeof Symbol ? Symbol : {}, a = i.iterator || "@@iterator", c = i.asyncIterator || "@@asyncIterator", u = i.toStringTag || "@@toStringTag"; function define(t, e, r) { return Object.defineProperty(t, e, { value: r, enumerable: !0, configurable: !0, writable: !0 }), t[e]; } try { define({}, ""); } catch (t) { define = function define(t, e, r) { return t[e] = r; }; } function wrap(t, e, r, n) { var i = e && e.prototype instanceof Generator ? e : Generator, a = Object.create(i.prototype), c = new Context(n || []); return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a; } function tryCatch(t, e, r) { try { return { type: "normal", arg: t.call(e, r) }; } catch (t) { return { type: "throw", arg: t }; } } e.wrap = wrap; var h = "suspendedStart", l = "suspendedYield", f = "executing", s = "completed", y = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var p = {}; define(p, a, function () { return this; }); var d = Object.getPrototypeOf, v = d && d(d(values([]))); v && v !== r && n.call(v, a) && (p = v); var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p); function defineIteratorMethods(t) { ["next", "throw", "return"].forEach(function (e) { define(t, e, function (t) { return this._invoke(e, t); }); }); } function AsyncIterator(t, e) { function invoke(r, o, i, a) { var c = tryCatch(t[r], t, o); if ("throw" !== c.type) { var u = c.arg, h = u.value; return h && "object" == _typeof(h) && n.call(h, "__await") ? e.resolve(h.__await).then(function (t) { invoke("next", t, i, a); }, function (t) { invoke("throw", t, i, a); }) : e.resolve(h).then(function (t) { u.value = t, i(u); }, function (t) { return invoke("throw", t, i, a); }); } a(c.arg); } var r; o(this, "_invoke", { value: function value(t, n) { function callInvokeWithMethodAndArg() { return new e(function (e, r) { invoke(t, n, e, r); }); } return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(e, r, n) { var o = h; return function (i, a) { if (o === f) throw new Error("Generator is already running"); if (o === s) { if ("throw" === i) throw a; return { value: t, done: !0 }; } for (n.method = i, n.arg = a;;) { var c = n.delegate; if (c) { var u = maybeInvokeDelegate(c, n); if (u) { if (u === y) continue; return u; } } if ("next" === n.method) n.sent = n._sent = n.arg;else if ("throw" === n.method) { if (o === h) throw o = s, n.arg; n.dispatchException(n.arg); } else "return" === n.method && n.abrupt("return", n.arg); o = f; var p = tryCatch(e, r, n); if ("normal" === p.type) { if (o = n.done ? s : l, p.arg === y) continue; return { value: p.arg, done: n.done }; } "throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg); } }; } function maybeInvokeDelegate(e, r) { var n = r.method, o = e.iterator[n]; if (o === t) return r.delegate = null, "throw" === n && e.iterator.return && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y; var i = tryCatch(o, e.iterator, r.arg); if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y; var a = i.arg; return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y); } function pushTryEntry(t) { var e = { tryLoc: t[0] }; 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e); } function resetTryEntry(t) { var e = t.completion || {}; e.type = "normal", delete e.arg, t.completion = e; } function Context(t) { this.tryEntries = [{ tryLoc: "root" }], t.forEach(pushTryEntry, this), this.reset(!0); } function values(e) { if (e || "" === e) { var r = e[a]; if (r) return r.call(e); if ("function" == typeof e.next) return e; if (!isNaN(e.length)) { var o = -1, i = function next() { for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next; return next.value = t, next.done = !0, next; }; return i.next = i; } } throw new TypeError(_typeof(e) + " is not iterable"); } return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", { value: GeneratorFunctionPrototype, configurable: !0 }), o(GeneratorFunctionPrototype, "constructor", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function (t) { var e = "function" == typeof t && t.constructor; return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name)); }, e.mark = function (t) { return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t; }, e.awrap = function (t) { return { __await: t }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () { return this; }), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) { void 0 === i && (i = Promise); var a = new AsyncIterator(wrap(t, r, n, o), i); return e.isGeneratorFunction(r) ? a : a.next().then(function (t) { return t.done ? t.value : a.next(); }); }, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function () { return this; }), define(g, "toString", function () { return "[object Generator]"; }), e.keys = function (t) { var e = Object(t), r = []; for (var n in e) r.push(n); return r.reverse(), function next() { for (; r.length;) { var t = r.pop(); if (t in e) return next.value = t, next.done = !1, next; } return next.done = !0, next; }; }, e.values = values, Context.prototype = { constructor: Context, reset: function reset(e) { if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t); }, stop: function stop() { this.done = !0; var t = this.tryEntries[0].completion; if ("throw" === t.type) throw t.arg; return this.rval; }, dispatchException: function dispatchException(e) { if (this.done) throw e; var r = this; function handle(n, o) { return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o; } for (var o = this.tryEntries.length - 1; o >= 0; --o) { var i = this.tryEntries[o], a = i.completion; if ("root" === i.tryLoc) return handle("end"); if (i.tryLoc <= this.prev) { var c = n.call(i, "catchLoc"), u = n.call(i, "finallyLoc"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw new Error("try statement without catch or finally"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) { var i = o; break; } } i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if ("throw" === t.type) throw t.arg; return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, catch: function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if ("throw" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, "next" === this.method && (this.arg = t), y; } }, e; }
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
1
4
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
5
|
+
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
6
|
+
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
2
7
|
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
|
|
3
8
|
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
4
9
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
@@ -28,4 +33,78 @@ export var HttpError = /*#__PURE__*/function (_Error) {
|
|
|
28
33
|
return _this;
|
|
29
34
|
}
|
|
30
35
|
return _createClass(HttpError);
|
|
31
|
-
}( /*#__PURE__*/_wrapNativeSuper(Error));
|
|
36
|
+
}( /*#__PURE__*/_wrapNativeSuper(Error));
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// 框架内建报错的文案 —— 默认中文,调用方可以按请求语言换掉
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// 这几句是从**库内部**抛出去的(准入判定、写动作的兜底),业务自己的报错在业务代码里
|
|
42
|
+
// 渲染(do-tv 是 (backend)/utils/i18n 的 serverT,抛出点就地翻成请求的语言)。库不知道
|
|
43
|
+
// 也不该知道各项目的 i18n 栈(next-intl、react-i18next、自研…),所以不给库塞字典,
|
|
44
|
+
// 只留一个「抛出前问一次」的钩子:谁做国际化,谁在应用启动时注册一个解析器。
|
|
45
|
+
//
|
|
46
|
+
// 为什么不回错误码让前端翻:这些 message 的读者不只是浏览器 —— CLI 与 agent 也读它,
|
|
47
|
+
// 它们没有前端那套翻译层,回个码等于让它们打印一串 `errors.UNAUTHENTICATED`。在服务端
|
|
48
|
+
// 渲染成字符串,所有读者拿到的都是人话。
|
|
49
|
+
//
|
|
50
|
+
// 解析器按**请求**解析(语言藏在 cookie / Accept-Language 里),所以它是每次抛出时现调
|
|
51
|
+
// 的函数,而不是启动时钉死的一份字符串。允许 async —— 各抛出点本来就在 async 函数里,
|
|
52
|
+
// 而「按当前请求取翻译」在 Next 里就得 await(do-tv 的 requestText() 走 next/headers)。
|
|
53
|
+
|
|
54
|
+
export var defaultHttpErrorMessages = {
|
|
55
|
+
unauthenticated: "未登录",
|
|
56
|
+
forbidden: "没有权限",
|
|
57
|
+
missingId: "缺少 id",
|
|
58
|
+
editNotFound: "未找到编辑对象,或没有权限",
|
|
59
|
+
deleteNotFound: "未找到删除对象,或没有权限",
|
|
60
|
+
scopeUnresolved: function scopeUnresolved(column) {
|
|
61
|
+
return "\u65E0\u6CD5\u786E\u5B9A\u8FD9\u6B21\u8BF7\u6C42\u7684\u5F52\u5C5E\uFF08".concat(column, "\uFF09\u3002\u8FD9\u4E0D\u662F\u300C\u4E0D\u505A\u9694\u79BB\u300D\u2014\u2014\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var resolver;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 注册文案解析器,应用启动时调一次(再次调用覆盖;传 undefined 恢复默认)。
|
|
68
|
+
* 不注册就一直是 defaultHttpErrorMessages 那几句中文 —— 没做国际化的项目零感知。
|
|
69
|
+
*/
|
|
70
|
+
export function setHttpErrorMessageResolver(next) {
|
|
71
|
+
resolver = next;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 取出当前生效的整份文案。**在抛出点现调**,别在模块顶层缓存 —— 解析器是按请求判
|
|
76
|
+
* 语言的,缓存住就把上一个请求的语言漏给下一个。
|
|
77
|
+
*
|
|
78
|
+
* 解析器自己抛了错(比如语言包没加载到)就回落默认中文:报错文案出不来不该把
|
|
79
|
+
* 原本那个 401/403 也吞掉。
|
|
80
|
+
*/
|
|
81
|
+
export function httpErrorMessages() {
|
|
82
|
+
return _httpErrorMessages.apply(this, arguments);
|
|
83
|
+
}
|
|
84
|
+
function _httpErrorMessages() {
|
|
85
|
+
_httpErrorMessages = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
|
|
86
|
+
var overrides, _resolver;
|
|
87
|
+
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
88
|
+
while (1) switch (_context.prev = _context.next) {
|
|
89
|
+
case 0:
|
|
90
|
+
_context.prev = 0;
|
|
91
|
+
_context.next = 3;
|
|
92
|
+
return (_resolver = resolver) === null || _resolver === void 0 ? void 0 : _resolver();
|
|
93
|
+
case 3:
|
|
94
|
+
overrides = _context.sent;
|
|
95
|
+
_context.next = 9;
|
|
96
|
+
break;
|
|
97
|
+
case 6:
|
|
98
|
+
_context.prev = 6;
|
|
99
|
+
_context.t0 = _context["catch"](0);
|
|
100
|
+
overrides = undefined;
|
|
101
|
+
case 9:
|
|
102
|
+
return _context.abrupt("return", _objectSpread(_objectSpread({}, defaultHttpErrorMessages), overrides));
|
|
103
|
+
case 10:
|
|
104
|
+
case "end":
|
|
105
|
+
return _context.stop();
|
|
106
|
+
}
|
|
107
|
+
}, _callee, null, [[0, 6]]);
|
|
108
|
+
}));
|
|
109
|
+
return _httpErrorMessages.apply(this, arguments);
|
|
110
|
+
}
|
|
@@ -8,7 +8,7 @@ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" ==
|
|
|
8
8
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
9
9
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
10
10
|
import { TypedNextResponse } from "@ai0x0/next-rest-framework";
|
|
11
|
-
import { HttpError } from "../errors.js";
|
|
11
|
+
import { HttpError, httpErrorMessages } from "../errors.js";
|
|
12
12
|
import { isEmptyScopeValue, NO_SCOPE } from "../scope.js";
|
|
13
13
|
|
|
14
14
|
// ==============================================================================
|
|
@@ -100,52 +100,83 @@ function _resolveAccess() {
|
|
|
100
100
|
case 9:
|
|
101
101
|
session = _context.t0;
|
|
102
102
|
if (!(access !== null && access !== void 0 && access.can)) {
|
|
103
|
-
_context.next =
|
|
103
|
+
_context.next = 33;
|
|
104
104
|
break;
|
|
105
105
|
}
|
|
106
106
|
if (session) {
|
|
107
|
-
_context.next =
|
|
107
|
+
_context.next = 17;
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
case 13:
|
|
110
|
+
_context.t1 = HttpError;
|
|
112
111
|
_context.next = 15;
|
|
112
|
+
return httpErrorMessages();
|
|
113
|
+
case 15:
|
|
114
|
+
_context.t2 = _context.sent.unauthenticated;
|
|
115
|
+
throw new _context.t1(401, _context.t2);
|
|
116
|
+
case 17:
|
|
117
|
+
_context.next = 19;
|
|
113
118
|
return access.can({
|
|
114
119
|
method: method,
|
|
115
120
|
session: session
|
|
116
121
|
});
|
|
117
|
-
case
|
|
122
|
+
case 19:
|
|
118
123
|
verdict = _context.sent;
|
|
119
124
|
if (!(verdict !== true)) {
|
|
120
|
-
_context.next =
|
|
125
|
+
_context.next = 33;
|
|
121
126
|
break;
|
|
122
127
|
}
|
|
123
128
|
denial = _typeof(verdict) === "object" ? verdict : {};
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
_context.t3 = HttpError;
|
|
130
|
+
_context.t4 = (_denial$status = denial.status) !== null && _denial$status !== void 0 ? _denial$status : 403;
|
|
131
|
+
if (!((_denial$message = denial.message) !== null && _denial$message !== void 0)) {
|
|
132
|
+
_context.next = 28;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
_context.t5 = _denial$message;
|
|
136
|
+
_context.next = 31;
|
|
137
|
+
break;
|
|
138
|
+
case 28:
|
|
139
|
+
_context.next = 30;
|
|
140
|
+
return httpErrorMessages();
|
|
141
|
+
case 30:
|
|
142
|
+
_context.t5 = _context.sent.forbidden;
|
|
143
|
+
case 31:
|
|
144
|
+
_context.t6 = _context.t5;
|
|
145
|
+
throw new _context.t3(_context.t4, _context.t6);
|
|
146
|
+
case 33:
|
|
126
147
|
if (wantsScope) {
|
|
127
|
-
_context.next =
|
|
148
|
+
_context.next = 35;
|
|
128
149
|
break;
|
|
129
150
|
}
|
|
130
151
|
return _context.abrupt("return", {
|
|
131
152
|
scope: NO_SCOPE,
|
|
132
153
|
session: session
|
|
133
154
|
});
|
|
134
|
-
case
|
|
155
|
+
case 35:
|
|
135
156
|
if (session) {
|
|
136
|
-
_context.next =
|
|
157
|
+
_context.next = 41;
|
|
137
158
|
break;
|
|
138
159
|
}
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
_context.t7 = HttpError;
|
|
161
|
+
_context.next = 39;
|
|
162
|
+
return httpErrorMessages();
|
|
163
|
+
case 39:
|
|
164
|
+
_context.t8 = _context.sent.unauthenticated;
|
|
165
|
+
throw new _context.t7(401, _context.t8);
|
|
166
|
+
case 41:
|
|
141
167
|
column = (_access$scope$column = access === null || access === void 0 || (_access$scope = access.scope) === null || _access$scope === void 0 ? void 0 : _access$scope.column) !== null && _access$scope$column !== void 0 ? _access$scope$column : DEFAULT_SCOPE_COLUMN;
|
|
142
168
|
value = access !== null && access !== void 0 && access.scope ? access.scope.value(session) : session.userId;
|
|
143
169
|
if (!isEmptyScopeValue(value)) {
|
|
144
|
-
_context.next =
|
|
170
|
+
_context.next = 49;
|
|
145
171
|
break;
|
|
146
172
|
}
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
_context.t9 = HttpError;
|
|
174
|
+
_context.next = 47;
|
|
175
|
+
return httpErrorMessages();
|
|
176
|
+
case 47:
|
|
177
|
+
_context.t10 = _context.sent.scopeUnresolved(column);
|
|
178
|
+
throw new _context.t9(403, _context.t10);
|
|
179
|
+
case 49:
|
|
149
180
|
return _context.abrupt("return", {
|
|
150
181
|
scope: {
|
|
151
182
|
column: column,
|
|
@@ -153,7 +184,7 @@ function _resolveAccess() {
|
|
|
153
184
|
},
|
|
154
185
|
session: session
|
|
155
186
|
});
|
|
156
|
-
case
|
|
187
|
+
case 50:
|
|
157
188
|
case "end":
|
|
158
189
|
return _context.stop();
|
|
159
190
|
}
|
|
@@ -39,11 +39,11 @@ function createPutActionCore({
|
|
|
39
39
|
return async (body, { scope }) => {
|
|
40
40
|
const id = body.id;
|
|
41
41
|
if (!id) {
|
|
42
|
-
throw new import_errors.HttpError(400,
|
|
42
|
+
throw new import_errors.HttpError(400, (await (0, import_errors.httpErrorMessages)()).missingId);
|
|
43
43
|
}
|
|
44
44
|
const [data] = await db.update(table).set((0, import_transform_body.transformBody)(body)).where(byId(table, id, scope)).returning();
|
|
45
45
|
if (!data && scope !== import_scope.NO_SCOPE) {
|
|
46
|
-
throw new import_errors.HttpError(404,
|
|
46
|
+
throw new import_errors.HttpError(404, (await (0, import_errors.httpErrorMessages)()).editNotFound);
|
|
47
47
|
}
|
|
48
48
|
return data;
|
|
49
49
|
};
|
|
@@ -54,11 +54,11 @@ function createDeleteActionCore({
|
|
|
54
54
|
}) {
|
|
55
55
|
return async ({ id }, { scope }) => {
|
|
56
56
|
if (!id) {
|
|
57
|
-
throw new import_errors.HttpError(400,
|
|
57
|
+
throw new import_errors.HttpError(400, (await (0, import_errors.httpErrorMessages)()).missingId);
|
|
58
58
|
}
|
|
59
59
|
const rows = await db.delete(table).where(byId(table, id, scope)).returning();
|
|
60
60
|
if (rows.length === 0 && scope !== import_scope.NO_SCOPE) {
|
|
61
|
-
throw new import_errors.HttpError(404,
|
|
61
|
+
throw new import_errors.HttpError(404, (await (0, import_errors.httpErrorMessages)()).deleteNotFound);
|
|
62
62
|
}
|
|
63
63
|
return rows;
|
|
64
64
|
};
|
package/lib/backend/errors.d.ts
CHANGED
|
@@ -2,3 +2,33 @@ export declare class HttpError extends Error {
|
|
|
2
2
|
status: number;
|
|
3
3
|
constructor(status: number, message: string);
|
|
4
4
|
}
|
|
5
|
+
export interface HttpErrorMessages {
|
|
6
|
+
/** 401:会话不存在。 */
|
|
7
|
+
unauthenticated: string;
|
|
8
|
+
/** 403:access.can 拒绝且没给自定义文案。 */
|
|
9
|
+
forbidden: string;
|
|
10
|
+
/** 400:PUT / DELETE 请求体缺 id。 */
|
|
11
|
+
missingId: string;
|
|
12
|
+
/** 404:PUT 影响 0 行(不存在或不属于调用者)。 */
|
|
13
|
+
editNotFound: string;
|
|
14
|
+
/** 404:DELETE 影响 0 行。 */
|
|
15
|
+
deleteNotFound: string;
|
|
16
|
+
/** 403:作用域值解析不出来 —— 多半是配错了列名,但请求照样要被拒绝。 */
|
|
17
|
+
scopeUnresolved: (column: string) => string;
|
|
18
|
+
}
|
|
19
|
+
export declare const defaultHttpErrorMessages: HttpErrorMessages;
|
|
20
|
+
type HttpErrorMessageResolver = () => Partial<HttpErrorMessages> | Promise<Partial<HttpErrorMessages>>;
|
|
21
|
+
/**
|
|
22
|
+
* 注册文案解析器,应用启动时调一次(再次调用覆盖;传 undefined 恢复默认)。
|
|
23
|
+
* 不注册就一直是 defaultHttpErrorMessages 那几句中文 —— 没做国际化的项目零感知。
|
|
24
|
+
*/
|
|
25
|
+
export declare function setHttpErrorMessageResolver(next: HttpErrorMessageResolver | undefined): void;
|
|
26
|
+
/**
|
|
27
|
+
* 取出当前生效的整份文案。**在抛出点现调**,别在模块顶层缓存 —— 解析器是按请求判
|
|
28
|
+
* 语言的,缓存住就把上一个请求的语言漏给下一个。
|
|
29
|
+
*
|
|
30
|
+
* 解析器自己抛了错(比如语言包没加载到)就回落默认中文:报错文案出不来不该把
|
|
31
|
+
* 原本那个 401/403 也吞掉。
|
|
32
|
+
*/
|
|
33
|
+
export declare function httpErrorMessages(): Promise<HttpErrorMessages>;
|
|
34
|
+
export {};
|
package/lib/backend/errors.js
CHANGED
|
@@ -19,7 +19,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/backend/errors.ts
|
|
20
20
|
var errors_exports = {};
|
|
21
21
|
__export(errors_exports, {
|
|
22
|
-
HttpError: () => HttpError
|
|
22
|
+
HttpError: () => HttpError,
|
|
23
|
+
defaultHttpErrorMessages: () => defaultHttpErrorMessages,
|
|
24
|
+
httpErrorMessages: () => httpErrorMessages,
|
|
25
|
+
setHttpErrorMessageResolver: () => setHttpErrorMessageResolver
|
|
23
26
|
});
|
|
24
27
|
module.exports = __toCommonJS(errors_exports);
|
|
25
28
|
var HttpError = class extends Error {
|
|
@@ -29,7 +32,31 @@ var HttpError = class extends Error {
|
|
|
29
32
|
this.status = status;
|
|
30
33
|
}
|
|
31
34
|
};
|
|
35
|
+
var defaultHttpErrorMessages = {
|
|
36
|
+
unauthenticated: "未登录",
|
|
37
|
+
forbidden: "没有权限",
|
|
38
|
+
missingId: "缺少 id",
|
|
39
|
+
editNotFound: "未找到编辑对象,或没有权限",
|
|
40
|
+
deleteNotFound: "未找到删除对象,或没有权限",
|
|
41
|
+
scopeUnresolved: (column) => `无法确定这次请求的归属(${column})。这不是「不做隔离」——请求被拒绝。`
|
|
42
|
+
};
|
|
43
|
+
var resolver;
|
|
44
|
+
function setHttpErrorMessageResolver(next) {
|
|
45
|
+
resolver = next;
|
|
46
|
+
}
|
|
47
|
+
async function httpErrorMessages() {
|
|
48
|
+
let overrides;
|
|
49
|
+
try {
|
|
50
|
+
overrides = await (resolver == null ? void 0 : resolver());
|
|
51
|
+
} catch {
|
|
52
|
+
overrides = void 0;
|
|
53
|
+
}
|
|
54
|
+
return { ...defaultHttpErrorMessages, ...overrides };
|
|
55
|
+
}
|
|
32
56
|
// Annotate the CommonJS export names for ESM import in node:
|
|
33
57
|
0 && (module.exports = {
|
|
34
|
-
HttpError
|
|
58
|
+
HttpError,
|
|
59
|
+
defaultHttpErrorMessages,
|
|
60
|
+
httpErrorMessages,
|
|
61
|
+
setHttpErrorMessageResolver
|
|
35
62
|
});
|
|
@@ -49,26 +49,29 @@ async function resolveAccess({
|
|
|
49
49
|
const session = wantsScope || (access == null ? void 0 : access.can) || (access == null ? void 0 : access.stamp) ? await loadSession() : void 0;
|
|
50
50
|
if (access == null ? void 0 : access.can) {
|
|
51
51
|
if (!session) {
|
|
52
|
-
throw new import_errors.HttpError(401,
|
|
52
|
+
throw new import_errors.HttpError(401, (await (0, import_errors.httpErrorMessages)()).unauthenticated);
|
|
53
53
|
}
|
|
54
54
|
const verdict = await access.can({ method, session });
|
|
55
55
|
if (verdict !== true) {
|
|
56
56
|
const denial = typeof verdict === "object" ? verdict : {};
|
|
57
|
-
throw new import_errors.HttpError(
|
|
57
|
+
throw new import_errors.HttpError(
|
|
58
|
+
denial.status ?? 403,
|
|
59
|
+
denial.message ?? (await (0, import_errors.httpErrorMessages)()).forbidden
|
|
60
|
+
);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
if (!wantsScope) {
|
|
61
64
|
return { scope: import_scope.NO_SCOPE, session };
|
|
62
65
|
}
|
|
63
66
|
if (!session) {
|
|
64
|
-
throw new import_errors.HttpError(401,
|
|
67
|
+
throw new import_errors.HttpError(401, (await (0, import_errors.httpErrorMessages)()).unauthenticated);
|
|
65
68
|
}
|
|
66
69
|
const column = ((_a = access == null ? void 0 : access.scope) == null ? void 0 : _a.column) ?? DEFAULT_SCOPE_COLUMN;
|
|
67
70
|
const value = (access == null ? void 0 : access.scope) ? access.scope.value(session) : session.userId;
|
|
68
71
|
if ((0, import_scope.isEmptyScopeValue)(value)) {
|
|
69
72
|
throw new import_errors.HttpError(
|
|
70
73
|
403,
|
|
71
|
-
|
|
74
|
+
(await (0, import_errors.httpErrorMessages)()).scopeUnresolved(column)
|
|
72
75
|
);
|
|
73
76
|
}
|
|
74
77
|
return { scope: { column, value }, session };
|