@ai0x0/utils 0.6.1 → 0.7.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/.agents/skills/ai0x0-utils/backend/SKILL.md +8 -5
- package/SKILL.md +8 -5
- package/es/backend/actions/create-get-action.js +11 -7
- package/es/backend/actions/create-get-list-action.js +11 -6
- package/es/backend/actions/get-list-data.d.ts +1 -1
- package/es/backend/actions/get-list-data.js +12 -5
- package/es/backend/actions/get-list-query-core.d.ts +26 -5
- package/es/backend/actions/get-list-query-core.js +92 -22
- package/es/backend/actions/get-list-query.d.ts +2 -0
- package/es/backend/actions/get-list-query.js +7 -4
- package/es/backend/actions/index.d.ts +1 -0
- package/es/backend/actions/index.js +2 -1
- package/es/backend/actions/paging.d.ts +11 -0
- package/es/backend/actions/paging.js +32 -0
- package/es/backend/route-operation/write-operation-core.js +12 -5
- package/es/backend/schemas/common.d.ts +2 -2
- package/es/backend/schemas/common.js +1 -1
- package/es/backend/sqlite/actions/create-get-action.js +9 -6
- package/es/backend/sqlite/actions/create-get-list-action.js +7 -5
- package/es/backend/sqlite/actions/get-list-data.d.ts +1 -1
- package/es/backend/sqlite/actions/get-list-data.js +11 -5
- package/es/backend/sqlite/actions/get-list-query.d.ts +2 -0
- package/es/backend/sqlite/actions/get-list-query.js +10 -6
- package/lib/backend/actions/create-get-action.js +3 -1
- package/lib/backend/actions/create-get-list-action.js +3 -1
- package/lib/backend/actions/get-list-data.d.ts +1 -1
- package/lib/backend/actions/get-list-data.js +8 -3
- package/lib/backend/actions/get-list-query-core.d.ts +26 -5
- package/lib/backend/actions/get-list-query-core.js +73 -18
- package/lib/backend/actions/get-list-query.d.ts +2 -0
- package/lib/backend/actions/get-list-query.js +3 -3
- package/lib/backend/actions/index.d.ts +1 -0
- package/lib/backend/actions/index.js +7 -2
- package/lib/backend/actions/paging.d.ts +11 -0
- package/lib/backend/actions/paging.js +50 -0
- package/lib/backend/schemas/common.d.ts +2 -2
- package/lib/backend/schemas/common.js +1 -1
- package/lib/backend/sqlite/actions/create-get-action.js +3 -1
- package/lib/backend/sqlite/actions/create-get-list-action.js +3 -1
- package/lib/backend/sqlite/actions/get-list-data.d.ts +1 -1
- package/lib/backend/sqlite/actions/get-list-data.js +8 -3
- package/lib/backend/sqlite/actions/get-list-query.d.ts +2 -0
- package/lib/backend/sqlite/actions/get-list-query.js +3 -3
- package/package.json +1 -1
|
@@ -18,14 +18,16 @@ export function createGetAction(_ref) {
|
|
|
18
18
|
table = _ref.table;
|
|
19
19
|
return /*#__PURE__*/function () {
|
|
20
20
|
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(params, _ref2) {
|
|
21
|
-
var scope, fields, _i,
|
|
21
|
+
var scope, exposed, fields, _i, _exposed, key, field, _getListQuery, query, countQuery, _yield$getListData, _yield$getListData$da, result;
|
|
22
22
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
23
23
|
while (1) switch (_context.prev = _context.next) {
|
|
24
24
|
case 0:
|
|
25
25
|
scope = _ref2.scope;
|
|
26
|
+
// 筛选面与可见面同源,理由见 backend/actions/create-get-list-action 里的同一段。
|
|
27
|
+
exposed = Object.keys(bodySchema.shape);
|
|
26
28
|
fields = {};
|
|
27
|
-
for (_i = 0,
|
|
28
|
-
key =
|
|
29
|
+
for (_i = 0, _exposed = exposed; _i < _exposed.length; _i++) {
|
|
30
|
+
key = _exposed[_i];
|
|
29
31
|
field = table[key];
|
|
30
32
|
if (field) {
|
|
31
33
|
fields[key] = field;
|
|
@@ -34,13 +36,14 @@ export function createGetAction(_ref) {
|
|
|
34
36
|
_getListQuery = getListQuery({
|
|
35
37
|
db: db,
|
|
36
38
|
fields: fields,
|
|
39
|
+
filterable: exposed,
|
|
37
40
|
jsonArrayFields: jsonArrayFields,
|
|
38
41
|
params: params,
|
|
39
42
|
scope: scope,
|
|
40
43
|
relations: relations,
|
|
41
44
|
table: table
|
|
42
45
|
}), query = _getListQuery.query, countQuery = _getListQuery.countQuery;
|
|
43
|
-
_context.next =
|
|
46
|
+
_context.next = 7;
|
|
44
47
|
return getListData({
|
|
45
48
|
bodySchema: bodySchema,
|
|
46
49
|
countQuery: countQuery,
|
|
@@ -48,12 +51,12 @@ export function createGetAction(_ref) {
|
|
|
48
51
|
pageSize: 1,
|
|
49
52
|
query: query
|
|
50
53
|
})();
|
|
51
|
-
case
|
|
54
|
+
case 7:
|
|
52
55
|
_yield$getListData = _context.sent;
|
|
53
56
|
_yield$getListData$da = _slicedToArray(_yield$getListData.data, 1);
|
|
54
57
|
result = _yield$getListData$da[0];
|
|
55
58
|
return _context.abrupt("return", result);
|
|
56
|
-
case
|
|
59
|
+
case 11:
|
|
57
60
|
case "end":
|
|
58
61
|
return _context.stop();
|
|
59
62
|
}
|
|
@@ -15,15 +15,16 @@ export function createGetListAction(_ref) {
|
|
|
15
15
|
table = _ref.table;
|
|
16
16
|
return /*#__PURE__*/function () {
|
|
17
17
|
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(params, _ref2) {
|
|
18
|
-
var scope, _ref4, current, pageSize, other, fields, _i,
|
|
18
|
+
var scope, _ref4, current, pageSize, other, exposed, fields, _i, _exposed, key, field, _getListQuery, query, countQuery;
|
|
19
19
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
20
20
|
while (1) switch (_context.prev = _context.next) {
|
|
21
21
|
case 0:
|
|
22
22
|
scope = _ref2.scope;
|
|
23
|
-
_ref4 = params, current = _ref4.current, pageSize = _ref4.pageSize, other = _objectWithoutProperties(_ref4, _excluded);
|
|
23
|
+
_ref4 = params, current = _ref4.current, pageSize = _ref4.pageSize, other = _objectWithoutProperties(_ref4, _excluded); // 筛选面与可见面同源,理由见 backend/actions/create-get-list-action 里的同一段。
|
|
24
|
+
exposed = Object.keys(bodySchema.shape);
|
|
24
25
|
fields = {};
|
|
25
|
-
for (_i = 0,
|
|
26
|
-
key =
|
|
26
|
+
for (_i = 0, _exposed = exposed; _i < _exposed.length; _i++) {
|
|
27
|
+
key = _exposed[_i];
|
|
27
28
|
field = table[key];
|
|
28
29
|
if (field) {
|
|
29
30
|
fields[key] = field;
|
|
@@ -32,6 +33,7 @@ export function createGetListAction(_ref) {
|
|
|
32
33
|
_getListQuery = getListQuery({
|
|
33
34
|
db: db,
|
|
34
35
|
fields: fields,
|
|
36
|
+
filterable: exposed,
|
|
35
37
|
jsonArrayFields: jsonArrayFields,
|
|
36
38
|
params: other,
|
|
37
39
|
scope: scope,
|
|
@@ -45,7 +47,7 @@ export function createGetListAction(_ref) {
|
|
|
45
47
|
pageSize: pageSize,
|
|
46
48
|
query: query
|
|
47
49
|
})());
|
|
48
|
-
case
|
|
50
|
+
case 7:
|
|
49
51
|
case "end":
|
|
50
52
|
return _context.stop();
|
|
51
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getListQuery } from "./get-list-query.js";
|
|
2
|
-
export declare function getListData({ query, countQuery, pageSize, current, }: {
|
|
2
|
+
export declare function getListData({ query, countQuery, pageSize: rawPageSize, current: rawCurrent, }: {
|
|
3
3
|
bodySchema?: any;
|
|
4
4
|
current?: number;
|
|
5
5
|
pageSize?: number;
|
|
@@ -8,6 +8,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
|
|
|
8
8
|
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
9
9
|
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); } }
|
|
10
10
|
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); }); }; }
|
|
11
|
+
import { normalizePaging } from "../../actions/paging.js";
|
|
11
12
|
function runSelect(_x) {
|
|
12
13
|
return _runSelect.apply(this, arguments);
|
|
13
14
|
}
|
|
@@ -42,16 +43,21 @@ function _runSelect() {
|
|
|
42
43
|
export function getListData(_ref) {
|
|
43
44
|
var query = _ref.query,
|
|
44
45
|
countQuery = _ref.countQuery,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
rawPageSize = _ref.pageSize,
|
|
47
|
+
rawCurrent = _ref.current;
|
|
48
|
+
// 归一化的理由见 backend/actions/paging。
|
|
49
|
+
var _normalizePaging = normalizePaging({
|
|
50
|
+
current: rawCurrent,
|
|
51
|
+
pageSize: rawPageSize
|
|
52
|
+
}),
|
|
53
|
+
current = _normalizePaging.current,
|
|
54
|
+
pageSize = _normalizePaging.pageSize;
|
|
49
55
|
return /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
|
|
50
56
|
var dataQuery, data, _yield$runSelect, _yield$runSelect2, count;
|
|
51
57
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
52
58
|
while (1) switch (_context.prev = _context.next) {
|
|
53
59
|
case 0:
|
|
54
|
-
dataQuery = query.limit(
|
|
60
|
+
dataQuery = query.limit(pageSize).offset((current - 1) * pageSize);
|
|
55
61
|
_context.next = 3;
|
|
56
62
|
return runSelect(dataQuery);
|
|
57
63
|
case 3:
|
|
@@ -5,6 +5,8 @@ import { type ListQueryParams } from "../../actions/get-list-query-core.js";
|
|
|
5
5
|
export declare function getListQuery<TTable extends BaseTable, TSelection extends SelectedFields>(args: {
|
|
6
6
|
db: any;
|
|
7
7
|
fields: TSelection;
|
|
8
|
+
/** 允许被当成筛选条件的键。不传等于不限制,见核心里的说明。 */
|
|
9
|
+
filterable?: readonly string[];
|
|
8
10
|
jsonArrayFields?: string[];
|
|
9
11
|
/** 必填。与 pg 那套同义,见 backend/scope.ts。 */
|
|
10
12
|
scope: ScopeArg;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _templateObject;
|
|
1
|
+
var _templateObject, _templateObject2;
|
|
2
2
|
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
|
|
3
3
|
// ==============================================================================
|
|
4
4
|
// 列表查询(SQLite)
|
|
@@ -6,16 +6,20 @@ function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(
|
|
|
6
6
|
// 实现在 backend/actions/get-list-query-core,与 pg 那套共用。这里只给它两条 sqlite 专属的
|
|
7
7
|
// 原语:sqlite 的 like 本来就不区分大小写(没有 ilike),JSON 数组用 json_each 展开。
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { sql } from "drizzle-orm";
|
|
10
10
|
import { createGetListQuery } from "../../actions/get-list-query-core.js";
|
|
11
|
+
|
|
12
|
+
// pattern 由核心转义好再传进来(见 escapeLikePattern),但**转义符要自己声明**:sqlite 的 LIKE
|
|
13
|
+
// 不带 ESCAPE 子句时一个转义符都没有,那样转义等于没做,`%` 照旧是通配符。这也是这里不能用
|
|
14
|
+
// drizzle 的 like() 助手的原因 —— 它拼不出 ESCAPE。
|
|
11
15
|
var run = createGetListQuery({
|
|
12
|
-
contains: function contains(column,
|
|
13
|
-
return
|
|
16
|
+
contains: function contains(column, pattern) {
|
|
17
|
+
return sql(_templateObject || (_templateObject = _taggedTemplateLiteral(["", " LIKE ", " ESCAPE '\\'"], ["", " LIKE ", " ESCAPE '\\\\'"])), column, pattern);
|
|
14
18
|
},
|
|
15
19
|
// json_each 展开出来的行里,值在 tag.value 上(pg 那个 _text 变体是直接给字符串的)——
|
|
16
20
|
// 这是两边唯一一处连字段名都不同的地方。
|
|
17
|
-
jsonArrayContains: function jsonArrayContains(column,
|
|
18
|
-
return sql(
|
|
21
|
+
jsonArrayContains: function jsonArrayContains(column, pattern) {
|
|
22
|
+
return sql(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n EXISTS (\n SELECT 1 FROM json_each(", ") tag\n WHERE tag.value LIKE ", " ESCAPE '\\'\n )\n "], ["\n EXISTS (\n SELECT 1 FROM json_each(", ") tag\n WHERE tag.value LIKE ", " ESCAPE '\\\\'\n )\n "])), column, pattern);
|
|
19
23
|
}
|
|
20
24
|
});
|
|
21
25
|
export function getListQuery(args) {
|
|
@@ -32,8 +32,9 @@ function createGetAction({
|
|
|
32
32
|
table
|
|
33
33
|
}) {
|
|
34
34
|
return async (params, { scope }) => {
|
|
35
|
+
const exposed = Object.keys(bodySchema.shape);
|
|
35
36
|
const fields = {};
|
|
36
|
-
for (const key of
|
|
37
|
+
for (const key of exposed) {
|
|
37
38
|
const field = table[key];
|
|
38
39
|
if (field) {
|
|
39
40
|
fields[key] = field;
|
|
@@ -42,6 +43,7 @@ function createGetAction({
|
|
|
42
43
|
const { query, countQuery } = (0, import_get_list_query.getListQuery)({
|
|
43
44
|
db,
|
|
44
45
|
fields,
|
|
46
|
+
filterable: exposed,
|
|
45
47
|
jsonArrayFields,
|
|
46
48
|
params,
|
|
47
49
|
relations,
|
|
@@ -33,8 +33,9 @@ function createGetListAction({
|
|
|
33
33
|
}) {
|
|
34
34
|
return async (params, { scope }) => {
|
|
35
35
|
const { current, pageSize, ...other } = params;
|
|
36
|
+
const exposed = Object.keys(bodySchema.shape);
|
|
36
37
|
const fields = {};
|
|
37
|
-
for (const key of
|
|
38
|
+
for (const key of exposed) {
|
|
38
39
|
const field = table[key];
|
|
39
40
|
if (field) {
|
|
40
41
|
fields[key] = field;
|
|
@@ -43,6 +44,7 @@ function createGetListAction({
|
|
|
43
44
|
const { query, countQuery } = (0, import_get_list_query.getListQuery)({
|
|
44
45
|
db,
|
|
45
46
|
fields,
|
|
47
|
+
filterable: exposed,
|
|
46
48
|
jsonArrayFields,
|
|
47
49
|
params: other,
|
|
48
50
|
relations,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodSchema } from "zod";
|
|
2
2
|
import { getListQuery } from "./get-list-query.js";
|
|
3
|
-
export declare function getListData<B extends ZodSchema>({ query, countQuery, pageSize, current, bodySchema, }: {
|
|
3
|
+
export declare function getListData<B extends ZodSchema>({ query, countQuery, pageSize: rawPageSize, current: rawCurrent, bodySchema, }: {
|
|
4
4
|
bodySchema: B;
|
|
5
5
|
current?: number;
|
|
6
6
|
pageSize?: number;
|
|
@@ -24,20 +24,25 @@ __export(get_list_data_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(get_list_data_exports);
|
|
25
25
|
var import_next_rest_framework = require("next-rest-framework");
|
|
26
26
|
var import_schemas = require("../schemas");
|
|
27
|
+
var import_paging = require("./paging");
|
|
27
28
|
function getListData({
|
|
28
29
|
query,
|
|
29
30
|
countQuery,
|
|
30
|
-
pageSize
|
|
31
|
-
current
|
|
31
|
+
pageSize: rawPageSize,
|
|
32
|
+
current: rawCurrent,
|
|
32
33
|
bodySchema
|
|
33
34
|
}) {
|
|
35
|
+
const { current, pageSize } = (0, import_paging.normalizePaging)({
|
|
36
|
+
current: rawCurrent,
|
|
37
|
+
pageSize: rawPageSize
|
|
38
|
+
});
|
|
34
39
|
return (0, import_next_rest_framework.rpcOperation)().outputs([
|
|
35
40
|
{
|
|
36
41
|
body: (0, import_schemas.listBodySchema)(bodySchema),
|
|
37
42
|
contentType: "application/json"
|
|
38
43
|
}
|
|
39
44
|
]).handler(async () => {
|
|
40
|
-
const data = await query.limit(
|
|
45
|
+
const data = await query.limit(pageSize).offset((current - 1) * pageSize).execute();
|
|
41
46
|
const [{ count }] = await countQuery.execute();
|
|
42
47
|
return { data, total: Number(count) };
|
|
43
48
|
});
|
|
@@ -15,10 +15,14 @@ export interface ListQueryRelation {
|
|
|
15
15
|
}
|
|
16
16
|
/** 两套方言之间**全部**的差异。加第三种数据库时要写的也只有这两条。 */
|
|
17
17
|
export interface ListQueryDialect {
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 大小写不敏感的「包含」。收到的 pattern 是**已经转义、已经带好 `%` 的完整模式串** ——
|
|
20
|
+
* 转义收在核心里做(见 escapeLikePattern),免得加第三种方言时漏掉那一步:漏掉不会报错,
|
|
21
|
+
* 只会让 `%` 重新变回通配符。
|
|
22
|
+
*/
|
|
23
|
+
contains(_column: Column, _pattern: string): SQL;
|
|
24
|
+
/** 「这个 JSON 数组列里有没有元素匹配这个模式」。pattern 同上,已转义。 */
|
|
25
|
+
jsonArrayContains(_column: Column, _pattern: string): SQL;
|
|
22
26
|
}
|
|
23
27
|
export interface ListQueryParams {
|
|
24
28
|
[field: string]: unknown;
|
|
@@ -27,9 +31,26 @@ export interface ListQueryParams {
|
|
|
27
31
|
page?: number;
|
|
28
32
|
pageSize?: number;
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 把用户输入变成 LIKE 模式里的**字面量**。
|
|
36
|
+
*
|
|
37
|
+
* 不转义的话 `?name=%` 就是「匹配全部」、`?name=a_c` 里的下划线是「任意一个字符」—— 前者只是
|
|
38
|
+
* 筛不准,后者配合「能按某一列筛」就是一个逐字符试探值的通道。反斜杠自己也要转,否则
|
|
39
|
+
* `?name=\` 会把后面那个 `%` 吃掉。
|
|
40
|
+
*/
|
|
41
|
+
export declare function escapeLikePattern(keyword: string): string;
|
|
42
|
+
export declare function createGetListQuery(dialect: ListQueryDialect): <TTable extends ListQueryTable, TSelection extends Record<string, unknown>>({ db, fields, filterable, jsonArrayFields, params, relations, scope, table, }: {
|
|
31
43
|
db: any;
|
|
32
44
|
fields: TSelection;
|
|
45
|
+
/**
|
|
46
|
+
* 允许被当成筛选条件的键。**不传等于不限制**(直接调 getListQuery 的老调用方行为不变)。
|
|
47
|
+
*
|
|
48
|
+
* 为什么需要它:筛选是按列名在表上找列的,而那跟「这个端点返回哪些列」完全脱钩 —— 于是
|
|
49
|
+
* 一个只返回 id / title 的列表,照样可以 `?secret=sk-a` 让数据库去 `ilike '%sk-a%'`,再从
|
|
50
|
+
* 返回的 total 上把这一列逐字符读出来。列表工厂传进来的是响应 schema 的字段集,也就是
|
|
51
|
+
* 「调用方本来就读得到的那些列」,筛选面与可见面因此对齐。
|
|
52
|
+
*/
|
|
53
|
+
filterable?: readonly string[] | undefined;
|
|
33
54
|
jsonArrayFields?: string[] | undefined;
|
|
34
55
|
/**
|
|
35
56
|
* 行级作用域。**必填**,不想隔离就显式传 NO_SCOPE —— 写成可选的话,漏传就退化成查全表,
|
|
@@ -19,18 +19,32 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/backend/actions/get-list-query-core.ts
|
|
20
20
|
var get_list_query_core_exports = {};
|
|
21
21
|
__export(get_list_query_core_exports, {
|
|
22
|
-
createGetListQuery: () => createGetListQuery
|
|
22
|
+
createGetListQuery: () => createGetListQuery,
|
|
23
|
+
escapeLikePattern: () => escapeLikePattern
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(get_list_query_core_exports);
|
|
25
26
|
var import_drizzle_orm = require("drizzle-orm");
|
|
27
|
+
var import_errors = require("../errors");
|
|
26
28
|
var import_scope = require("../scope");
|
|
29
|
+
var import_paging = require("./paging");
|
|
30
|
+
function escapeLikePattern(keyword) {
|
|
31
|
+
return `%${keyword.replace(/[\\%_]/g, (char) => `\\${char}`)}%`;
|
|
32
|
+
}
|
|
27
33
|
function columnAt(table, key) {
|
|
28
|
-
|
|
34
|
+
const value = table[key];
|
|
35
|
+
return (0, import_drizzle_orm.is)(value, import_drizzle_orm.Column) ? value : void 0;
|
|
36
|
+
}
|
|
37
|
+
function isIdKey(key) {
|
|
38
|
+
return key === "id" || key.endsWith("Id");
|
|
39
|
+
}
|
|
40
|
+
function isFuzzyMatchable(column) {
|
|
41
|
+
return column.dataType === "string" && !/uuid|enum/i.test(column.columnType);
|
|
29
42
|
}
|
|
30
43
|
function createGetListQuery(dialect) {
|
|
31
44
|
return function getListQuery({
|
|
32
45
|
db,
|
|
33
46
|
fields,
|
|
47
|
+
filterable,
|
|
34
48
|
jsonArrayFields,
|
|
35
49
|
params,
|
|
36
50
|
relations,
|
|
@@ -38,12 +52,18 @@ function createGetListQuery(dialect) {
|
|
|
38
52
|
table
|
|
39
53
|
}) {
|
|
40
54
|
const {
|
|
41
|
-
page
|
|
42
|
-
pageSize
|
|
43
|
-
orderBy
|
|
55
|
+
page,
|
|
56
|
+
pageSize: rawPageSize,
|
|
57
|
+
orderBy,
|
|
44
58
|
orderDir = "desc",
|
|
45
59
|
...filters
|
|
46
60
|
} = params;
|
|
61
|
+
const { current, pageSize } = (0, import_paging.normalizePaging)({
|
|
62
|
+
current: page,
|
|
63
|
+
pageSize: rawPageSize
|
|
64
|
+
});
|
|
65
|
+
const allowed = filterable ? new Set(filterable) : void 0;
|
|
66
|
+
const isFilterable = (key) => !allowed || allowed.has(key);
|
|
47
67
|
function buildBaseQuery() {
|
|
48
68
|
let queryFields = { ...fields };
|
|
49
69
|
if (relations == null ? void 0 : relations.length) {
|
|
@@ -66,7 +86,7 @@ function createGetListQuery(dialect) {
|
|
|
66
86
|
).map((candidate) => columnAt(candidate, key)).find(Boolean);
|
|
67
87
|
}
|
|
68
88
|
function addCondition(conditions2, key, value, targetColumn) {
|
|
69
|
-
const
|
|
89
|
+
const isExact = isIdKey(key) || !isFuzzyMatchable(targetColumn);
|
|
70
90
|
const isJsonArray = jsonArrayFields == null ? void 0 : jsonArrayFields.includes(key);
|
|
71
91
|
if (typeof value !== "string") {
|
|
72
92
|
conditions2.push((0, import_drizzle_orm.eq)(targetColumn, value));
|
|
@@ -78,24 +98,35 @@ function createGetListQuery(dialect) {
|
|
|
78
98
|
return;
|
|
79
99
|
}
|
|
80
100
|
if (isJsonArray) {
|
|
81
|
-
conditions2.push(
|
|
82
|
-
|
|
101
|
+
conditions2.push(
|
|
102
|
+
dialect.jsonArrayContains(
|
|
103
|
+
targetColumn,
|
|
104
|
+
escapeLikePattern(values[0])
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
} else if (isExact) {
|
|
83
108
|
conditions2.push((0, import_drizzle_orm.inArray)(targetColumn, values));
|
|
84
109
|
} else {
|
|
85
110
|
conditions2.push(
|
|
86
111
|
(0, import_drizzle_orm.or)(
|
|
87
|
-
...values.map(
|
|
112
|
+
...values.map(
|
|
113
|
+
(item) => dialect.contains(targetColumn, escapeLikePattern(item))
|
|
114
|
+
)
|
|
88
115
|
)
|
|
89
116
|
);
|
|
90
117
|
}
|
|
91
118
|
return;
|
|
92
119
|
}
|
|
93
120
|
if (isJsonArray) {
|
|
94
|
-
conditions2.push(
|
|
95
|
-
|
|
121
|
+
conditions2.push(
|
|
122
|
+
dialect.jsonArrayContains(targetColumn, escapeLikePattern(value))
|
|
123
|
+
);
|
|
124
|
+
} else if (isExact) {
|
|
96
125
|
conditions2.push((0, import_drizzle_orm.eq)(targetColumn, value));
|
|
97
126
|
} else {
|
|
98
|
-
conditions2.push(
|
|
127
|
+
conditions2.push(
|
|
128
|
+
dialect.contains(targetColumn, escapeLikePattern(value))
|
|
129
|
+
);
|
|
99
130
|
}
|
|
100
131
|
}
|
|
101
132
|
function buildConditions() {
|
|
@@ -105,16 +136,29 @@ function createGetListQuery(dialect) {
|
|
|
105
136
|
continue;
|
|
106
137
|
}
|
|
107
138
|
if (key.endsWith("AtFrom") || key.endsWith("AtTo")) {
|
|
108
|
-
const baseFieldName = key.replace(/(AtFrom|AtTo)$/, "")
|
|
109
|
-
|
|
139
|
+
const baseFieldName = `${key.replace(/(AtFrom|AtTo)$/, "")}At`;
|
|
140
|
+
if (!isFilterable(baseFieldName)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const targetColumn2 = findTargetColumn(baseFieldName);
|
|
110
144
|
if (!targetColumn2) {
|
|
111
145
|
continue;
|
|
112
146
|
}
|
|
147
|
+
const at = new Date(value);
|
|
148
|
+
if (Number.isNaN(at.getTime())) {
|
|
149
|
+
throw new import_errors.HttpError(
|
|
150
|
+
400,
|
|
151
|
+
`${key} 不是一个能识别的时间,请用 ISO 8601(如 2026-08-01T00:00:00Z)。`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
113
154
|
conditions2.push(
|
|
114
|
-
key.endsWith("AtFrom") ? (0, import_drizzle_orm.gte)(targetColumn2,
|
|
155
|
+
key.endsWith("AtFrom") ? (0, import_drizzle_orm.gte)(targetColumn2, at) : (0, import_drizzle_orm.lte)(targetColumn2, at)
|
|
115
156
|
);
|
|
116
157
|
continue;
|
|
117
158
|
}
|
|
159
|
+
if (!isFilterable(key)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
118
162
|
const targetColumn = findTargetColumn(key);
|
|
119
163
|
if (!targetColumn) {
|
|
120
164
|
continue;
|
|
@@ -124,7 +168,17 @@ function createGetListQuery(dialect) {
|
|
|
124
168
|
return conditions2;
|
|
125
169
|
}
|
|
126
170
|
function buildOrderBy() {
|
|
127
|
-
const
|
|
171
|
+
const requested = typeof orderBy === "string" && orderBy ? orderBy : void 0;
|
|
172
|
+
const orderKey = requested ?? "createdAt";
|
|
173
|
+
const orderColumn = columnAt(table, orderKey);
|
|
174
|
+
if (!orderColumn) {
|
|
175
|
+
if (requested) {
|
|
176
|
+
throw new import_errors.HttpError(400, `orderBy "${requested}" 不是这张表上的列。`);
|
|
177
|
+
}
|
|
178
|
+
throw new Error(
|
|
179
|
+
`默认排序列 createdAt 不在这张表上。给这张表的列表查询显式传一个 orderBy。`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
128
182
|
return orderDir === "asc" ? (0, import_drizzle_orm.asc)(orderColumn) : (0, import_drizzle_orm.desc)(orderColumn);
|
|
129
183
|
}
|
|
130
184
|
const baseQuery = buildBaseQuery();
|
|
@@ -132,7 +186,7 @@ function createGetListQuery(dialect) {
|
|
|
132
186
|
const conditions = scopeSql ? [scopeSql] : [];
|
|
133
187
|
conditions.push(...buildConditions());
|
|
134
188
|
const where = conditions.length > 0 ? (0, import_drizzle_orm.and)(...conditions) : void 0;
|
|
135
|
-
const query = baseQuery.where(where).orderBy(buildOrderBy()).limit(pageSize).offset((
|
|
189
|
+
const query = baseQuery.where(where).orderBy(buildOrderBy()).limit(pageSize).offset((current - 1) * pageSize);
|
|
136
190
|
const countQuery = db.select({ count: import_drizzle_orm.sql`COUNT(DISTINCT ${table.id})` }).from(table).where(where);
|
|
137
191
|
if (relations == null ? void 0 : relations.length) {
|
|
138
192
|
for (const { table: relationTable, sql: joinSql } of relations) {
|
|
@@ -147,5 +201,6 @@ function createGetListQuery(dialect) {
|
|
|
147
201
|
}
|
|
148
202
|
// Annotate the CommonJS export names for ESM import in node:
|
|
149
203
|
0 && (module.exports = {
|
|
150
|
-
createGetListQuery
|
|
204
|
+
createGetListQuery,
|
|
205
|
+
escapeLikePattern
|
|
151
206
|
});
|
|
@@ -6,6 +6,8 @@ import { type ListQueryParams } from "./get-list-query-core.js";
|
|
|
6
6
|
export declare function getListQuery<TTable extends BaseTable, TSelection extends SelectedFields>(args: {
|
|
7
7
|
db: NodePgDatabase<Record<string, unknown>>;
|
|
8
8
|
fields: TSelection;
|
|
9
|
+
/** 允许被当成筛选条件的键。不传等于不限制,见核心里的说明。 */
|
|
10
|
+
filterable?: readonly string[];
|
|
9
11
|
jsonArrayFields?: string[];
|
|
10
12
|
/**
|
|
11
13
|
* 行级作用域。**必填**,不想隔离就显式传 NO_SCOPE —— 写成可选的话,漏传就退化成查全表,
|
|
@@ -25,13 +25,13 @@ module.exports = __toCommonJS(get_list_query_exports);
|
|
|
25
25
|
var import_drizzle_orm = require("drizzle-orm");
|
|
26
26
|
var import_get_list_query_core = require("./get-list-query-core");
|
|
27
27
|
var run = (0, import_get_list_query_core.createGetListQuery)({
|
|
28
|
-
contains: (column,
|
|
28
|
+
contains: (column, pattern) => (0, import_drizzle_orm.ilike)(column, pattern),
|
|
29
29
|
// jsonb 数组展开成行再逐个 LIKE。列存的是 text[] 形状的 jsonb(见 createTableSchema 的
|
|
30
30
|
// jsonArrayFields),所以用 _text 那个变体,取到的 tag 直接就是字符串。
|
|
31
|
-
jsonArrayContains: (column,
|
|
31
|
+
jsonArrayContains: (column, pattern) => import_drizzle_orm.sql`
|
|
32
32
|
EXISTS (
|
|
33
33
|
SELECT 1 FROM jsonb_array_elements_text(${column}) tag
|
|
34
|
-
WHERE tag LIKE ${
|
|
34
|
+
WHERE tag LIKE ${pattern}
|
|
35
35
|
)
|
|
36
36
|
`
|
|
37
37
|
});
|
|
@@ -4,3 +4,4 @@ export { createGetListAction } from "./create-get-list-action.js";
|
|
|
4
4
|
export { createPostAction } from "./create-post-action.js";
|
|
5
5
|
export { createPutAction } from "./create-put-action.js";
|
|
6
6
|
export { getListQuery } from "./get-list-query.js";
|
|
7
|
+
export { MAX_PAGE_SIZE, normalizePaging } from "./paging.js";
|
|
@@ -19,12 +19,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/backend/actions/index.ts
|
|
20
20
|
var actions_exports = {};
|
|
21
21
|
__export(actions_exports, {
|
|
22
|
+
MAX_PAGE_SIZE: () => import_paging.MAX_PAGE_SIZE,
|
|
22
23
|
createDeleteAction: () => import_create_delete_action.createDeleteAction,
|
|
23
24
|
createGetAction: () => import_create_get_action.createGetAction,
|
|
24
25
|
createGetListAction: () => import_create_get_list_action.createGetListAction,
|
|
25
26
|
createPostAction: () => import_create_post_action.createPostAction,
|
|
26
27
|
createPutAction: () => import_create_put_action.createPutAction,
|
|
27
|
-
getListQuery: () => import_get_list_query.getListQuery
|
|
28
|
+
getListQuery: () => import_get_list_query.getListQuery,
|
|
29
|
+
normalizePaging: () => import_paging.normalizePaging
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(actions_exports);
|
|
30
32
|
var import_create_delete_action = require("./create-delete-action");
|
|
@@ -33,12 +35,15 @@ var import_create_get_list_action = require("./create-get-list-action");
|
|
|
33
35
|
var import_create_post_action = require("./create-post-action");
|
|
34
36
|
var import_create_put_action = require("./create-put-action");
|
|
35
37
|
var import_get_list_query = require("./get-list-query");
|
|
38
|
+
var import_paging = require("./paging");
|
|
36
39
|
// Annotate the CommonJS export names for ESM import in node:
|
|
37
40
|
0 && (module.exports = {
|
|
41
|
+
MAX_PAGE_SIZE,
|
|
38
42
|
createDeleteAction,
|
|
39
43
|
createGetAction,
|
|
40
44
|
createGetListAction,
|
|
41
45
|
createPostAction,
|
|
42
46
|
createPutAction,
|
|
43
|
-
getListQuery
|
|
47
|
+
getListQuery,
|
|
48
|
+
normalizePaging
|
|
44
49
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** pageSize 的硬上界。取这个量级是因为「一次把一整块数据取回来」是列表的正常用法。 */
|
|
2
|
+
export declare const MAX_PAGE_SIZE = 10000;
|
|
3
|
+
export declare const DEFAULT_PAGE_SIZE = 10;
|
|
4
|
+
/** 返回一定能安全喂给 limit / offset 的一对整数。 */
|
|
5
|
+
export declare function normalizePaging({ current, pageSize, }: {
|
|
6
|
+
current?: unknown;
|
|
7
|
+
pageSize?: unknown;
|
|
8
|
+
}): {
|
|
9
|
+
current: number;
|
|
10
|
+
pageSize: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/backend/actions/paging.ts
|
|
20
|
+
var paging_exports = {};
|
|
21
|
+
__export(paging_exports, {
|
|
22
|
+
DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
|
|
23
|
+
MAX_PAGE_SIZE: () => MAX_PAGE_SIZE,
|
|
24
|
+
normalizePaging: () => normalizePaging
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(paging_exports);
|
|
27
|
+
var MAX_PAGE_SIZE = 1e4;
|
|
28
|
+
var DEFAULT_PAGE_SIZE = 10;
|
|
29
|
+
function toPositiveInt(value, fallback) {
|
|
30
|
+
const parsed = Math.floor(Number(value));
|
|
31
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
32
|
+
}
|
|
33
|
+
function normalizePaging({
|
|
34
|
+
current,
|
|
35
|
+
pageSize
|
|
36
|
+
}) {
|
|
37
|
+
return {
|
|
38
|
+
current: toPositiveInt(current, 1),
|
|
39
|
+
pageSize: Math.min(
|
|
40
|
+
toPositiveInt(pageSize, DEFAULT_PAGE_SIZE),
|
|
41
|
+
MAX_PAGE_SIZE
|
|
42
|
+
)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
46
|
+
0 && (module.exports = {
|
|
47
|
+
DEFAULT_PAGE_SIZE,
|
|
48
|
+
MAX_PAGE_SIZE,
|
|
49
|
+
normalizePaging
|
|
50
|
+
});
|
|
@@ -32,7 +32,7 @@ export declare const BASIC_UPDATE_OMIT: {
|
|
|
32
32
|
readonly createdAt: true;
|
|
33
33
|
readonly updatedAt: true;
|
|
34
34
|
};
|
|
35
|
-
export declare const queryListSchema: <Incoming extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: Incoming) => z.ZodObject<(("
|
|
35
|
+
export declare const queryListSchema: <Incoming extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: Incoming) => z.ZodObject<(("current" | "pageSize" | "orderBy" | "orderDir" | "creatorId" | "createdAtFrom" | "createdAtTo") & keyof Incoming["shape"] extends never ? {
|
|
36
36
|
current: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
37
37
|
pageSize: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
38
38
|
createdAtFrom: z.ZodOptional<z.ZodString>;
|
|
@@ -65,7 +65,7 @@ export declare const queryListSchema: <Incoming extends z.ZodObject<z.core.$ZodL
|
|
|
65
65
|
asc: "asc";
|
|
66
66
|
desc: "desc";
|
|
67
67
|
}>>;
|
|
68
|
-
}[K]; } : never) & (Incoming["shape"] extends infer T_2 extends z.core.util.SomeObject ? { [K_1 in keyof T_2]: Incoming["shape"][K_1]; } : never)) extends infer T ? { [k in keyof T]: (("
|
|
68
|
+
}[K]; } : never) & (Incoming["shape"] extends infer T_2 extends z.core.util.SomeObject ? { [K_1 in keyof T_2]: Incoming["shape"][K_1]; } : never)) extends infer T ? { [k in keyof T]: (("current" | "pageSize" | "orderBy" | "orderDir" | "creatorId" | "createdAtFrom" | "createdAtTo") & keyof Incoming["shape"] extends never ? {
|
|
69
69
|
current: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
70
70
|
pageSize: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
71
71
|
createdAtFrom: z.ZodOptional<z.ZodString>;
|
|
@@ -46,7 +46,7 @@ var BASIC_UPDATE_OMIT = {
|
|
|
46
46
|
var queryListSchema = (schema) => import_zod.z.object({
|
|
47
47
|
current: import_zod.z.string().optional().default("1").describe("Page number, starting at 1."),
|
|
48
48
|
pageSize: import_zod.z.string().optional().default("10").describe(
|
|
49
|
-
"Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."
|
|
49
|
+
"Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set. Capped at 10000; compare `total` against what you received to tell whether more remain."
|
|
50
50
|
),
|
|
51
51
|
createdAtFrom: import_zod.z.string().optional().describe(
|
|
52
52
|
"Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."
|