@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
|
@@ -338,12 +338,14 @@ export const { GET } = route({
|
|
|
338
338
|
- **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`.
|
|
339
339
|
- **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.
|
|
340
340
|
- **Date coercion**: any body field ending in `At` is passed through `dayjs(value).toDate()` before write — accept ISO strings from clients.
|
|
341
|
-
- **List filtering** (all querystring values are strings)
|
|
342
|
-
- `key=a,b,c` → `IN (...)` when key ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
343
|
-
- `key=foo` where key ends with `Id` → `eq`; otherwise `ILIKE %foo%`
|
|
344
|
-
-
|
|
341
|
+
- **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.
|
|
342
|
+
- `key=a,b,c` → `IN (...)` when the key is `id` or ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
343
|
+
- `key=foo` where the key is `id` or ends with `Id` → `eq`; otherwise `ILIKE %foo%`
|
|
344
|
+
- non-text columns (uuid / number / date / enum) are always `eq` whatever the key looks like — `ILIKE` against them is a database type error, not an empty result
|
|
345
|
+
- `%` and `_` inside a value match literally; they are not wildcards
|
|
346
|
+
- `${anything}AtFrom` / `${anything}AtTo` → `gte` / `lte` against the matching `${anything}At` column; a value that is not a recognizable date is a 400
|
|
345
347
|
- `jsonArrayFields: ["tags"]` → `EXISTS (SELECT 1 FROM jsonb_array_elements_text(col) t WHERE t LIKE %v%)`
|
|
346
|
-
- **Pagination / sort**: `current` (default `"1"`), `pageSize` (default `"10"`), `orderBy` (default `createdAt
|
|
348
|
+
- **Pagination / sort**: `current` (default `"1"`), `pageSize` (default `"10"`, capped at `10000`, unparseable values fall back to the default — never to "no limit"), `orderBy` (default `createdAt`; must name a real column on the table, otherwise 400), `orderDir` (`asc|desc`, default `desc`).
|
|
347
349
|
- **Relations**: `relations: [{ table, sql, select, groupBy? }]` drives `leftJoin` + optional `groupBy(root.id, relation.id)`; `select` merges into the final `SELECT` projection and can contain raw `sql\`…\`` correlated sub-queries.
|
|
348
350
|
|
|
349
351
|
## Guardrails
|
|
@@ -351,6 +353,7 @@ export const { GET } = route({
|
|
|
351
353
|
- Always pass `insertXxxSchema` to POST, `updateXxxSchema` (has `.required({ id: true })`) to PUT, `selectXxxSchema` to GET body, `queryListSchema(selectXxxSchema.partial())` to GET_LIST query.
|
|
352
354
|
- Query-string schemas stay `z.string()` — numbers / arrays from querystrings must stay strings and be parsed inside `handler` or `setParams`.
|
|
353
355
|
- Do not reuse `insertSchema` for PUT — missing `id` silently dispatches an update without a where clause.
|
|
356
|
+
- A `contentType: "multipart/form-data"` POST is reachable from a cross-site HTML form; a JSON one is not (the framework answers 415 on a mismatched content type). If the app authenticates with cookies, gate those routes on an `Origin` check of your own.
|
|
354
357
|
- `schemas.response` on list/get is the **response item** schema, not the query shape. Query shape goes in `schemas.query`.
|
|
355
358
|
- Do not inline `{ db, getSession }` into individual route files — always import the pre-bound `postOperation` / `getListOperation` / … from `@backend/utils/route-operation` so session/DB swaps are one-file changes.
|
|
356
359
|
- `jsonb` columns declared with `jsonb(...)` carry no zod info — extend the matching insert/select schema with `.merge(z.object({ field: z.array(z.string()).optional() }))` to keep typing accurate.
|
package/SKILL.md
CHANGED
|
@@ -318,12 +318,14 @@ export const { GET } = route({
|
|
|
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
320
|
- **Date coercion**: any body field ending in `At` is passed through `dayjs(value).toDate()` before write — accept ISO strings from clients.
|
|
321
|
-
- **List filtering** (all querystring values are strings)
|
|
322
|
-
- `key=a,b,c` → `IN (...)` when key ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
323
|
-
- `key=foo` where key ends with `Id` → `eq`; otherwise `ILIKE %foo%`
|
|
324
|
-
-
|
|
321
|
+
- **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
|
+
- `key=a,b,c` → `IN (...)` when the key is `id` or ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
323
|
+
- `key=foo` where the key is `id` or ends with `Id` → `eq`; otherwise `ILIKE %foo%`
|
|
324
|
+
- non-text columns (uuid / number / date / enum) are always `eq` whatever the key looks like — `ILIKE` against them is a database type error, not an empty result
|
|
325
|
+
- `%` and `_` inside a value match literally; they are not wildcards
|
|
326
|
+
- `${anything}AtFrom` / `${anything}AtTo` → `gte` / `lte` against the matching `${anything}At` column; a value that is not a recognizable date is a 400
|
|
325
327
|
- `jsonArrayFields: ["tags"]` → `EXISTS (SELECT 1 FROM jsonb_array_elements_text(col) t WHERE t LIKE %v%)`
|
|
326
|
-
- **Pagination / sort**: `current` (default `"1"`), `pageSize` (default `"10"`), `orderBy` (default `createdAt
|
|
328
|
+
- **Pagination / sort**: `current` (default `"1"`), `pageSize` (default `"10"`, capped at `10000`, unparseable values fall back to the default — never to "no limit"), `orderBy` (default `createdAt`; must name a real column on the table, otherwise 400), `orderDir` (`asc|desc`, default `desc`).
|
|
327
329
|
- **Relations**: `relations: [{ table, sql, select, groupBy? }]` drives `leftJoin` + optional `groupBy(root.id, relation.id)`; `select` merges into the final `SELECT` projection and can contain raw `sql\`…\`` correlated sub-queries.
|
|
328
330
|
|
|
329
331
|
## Guardrails
|
|
@@ -331,6 +333,7 @@ export const { GET } = route({
|
|
|
331
333
|
- Always pass `insertXxxSchema` to POST, `updateXxxSchema` (has `.required({ id: true })`) to PUT, `selectXxxSchema` to GET body, `queryListSchema(selectXxxSchema.partial())` to GET_LIST query.
|
|
332
334
|
- Query-string schemas stay `z.string()` — numbers / arrays from querystrings must stay strings and be parsed inside `handler` or `setParams`.
|
|
333
335
|
- Do not reuse `insertSchema` for PUT — missing `id` silently dispatches an update without a where clause.
|
|
336
|
+
- A `contentType: "multipart/form-data"` POST is reachable from a cross-site HTML form; a JSON one is not (the framework answers 415 on a mismatched content type). If the app authenticates with cookies, gate those routes on an `Origin` check of your own.
|
|
334
337
|
- `schemas.response` on list/get is the **response item** schema, not the query shape. Query shape goes in `schemas.query`.
|
|
335
338
|
- Do not inline `{ db, getSession }` into individual route files — always import the pre-bound `postOperation` / `getListOperation` / … from `@backend/utils/route-operation` so session/DB swaps are one-file changes.
|
|
336
339
|
- `jsonb` columns declared with `jsonb(...)` carry no zod info — extend the matching insert/select schema with `.merge(z.object({ field: z.array(z.string()).optional() }))` to keep typing accurate.
|
|
@@ -18,14 +18,17 @@ 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// 筛选面与可见面同源,理由见 create-get-list-action 里的同一段。
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
exposed = Object.keys(bodySchema.shape);
|
|
29
|
+
fields = {};
|
|
30
|
+
for (_i = 0, _exposed = exposed; _i < _exposed.length; _i++) {
|
|
31
|
+
key = _exposed[_i];
|
|
29
32
|
// @ts-ignore
|
|
30
33
|
field = table[key];
|
|
31
34
|
if (field) {
|
|
@@ -35,13 +38,14 @@ export function createGetAction(_ref) {
|
|
|
35
38
|
_getListQuery = getListQuery({
|
|
36
39
|
db: db,
|
|
37
40
|
fields: fields,
|
|
41
|
+
filterable: exposed,
|
|
38
42
|
jsonArrayFields: jsonArrayFields,
|
|
39
43
|
params: params,
|
|
40
44
|
relations: relations,
|
|
41
45
|
scope: scope,
|
|
42
46
|
table: table
|
|
43
47
|
}), query = _getListQuery.query, countQuery = _getListQuery.countQuery;
|
|
44
|
-
_context.next =
|
|
48
|
+
_context.next = 7;
|
|
45
49
|
return getListData({
|
|
46
50
|
bodySchema: bodySchema,
|
|
47
51
|
countQuery: countQuery,
|
|
@@ -49,12 +53,12 @@ export function createGetAction(_ref) {
|
|
|
49
53
|
pageSize: 1,
|
|
50
54
|
query: query
|
|
51
55
|
})();
|
|
52
|
-
case
|
|
56
|
+
case 7:
|
|
53
57
|
_yield$getListData = _context.sent;
|
|
54
58
|
_yield$getListData$da = _slicedToArray(_yield$getListData.data, 1);
|
|
55
59
|
result = _yield$getListData$da[0];
|
|
56
60
|
return _context.abrupt("return", result);
|
|
57
|
-
case
|
|
61
|
+
case 11:
|
|
58
62
|
case "end":
|
|
59
63
|
return _context.stop();
|
|
60
64
|
}
|
|
@@ -15,15 +15,19 @@ 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);
|
|
24
|
-
fields
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
_ref4 = params, current = _ref4.current, pageSize = _ref4.pageSize, other = _objectWithoutProperties(_ref4, _excluded); // 响应 schema 的字段集 = 调用方读得到的那些列。它同时决定两件事:**查什么**(下面的
|
|
24
|
+
// fields)和**能按什么筛**(filterable)。两者必须是同一份 —— 否则一个只返回 id / title 的
|
|
25
|
+
// 列表照样能 `?secret=sk-a` 让数据库去 ilike,再从 total 上把那一列逐字符读出来。
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
exposed = Object.keys(bodySchema.shape);
|
|
28
|
+
fields = {};
|
|
29
|
+
for (_i = 0, _exposed = exposed; _i < _exposed.length; _i++) {
|
|
30
|
+
key = _exposed[_i];
|
|
27
31
|
// @ts-ignore
|
|
28
32
|
field = table[key];
|
|
29
33
|
if (field) {
|
|
@@ -33,6 +37,7 @@ export function createGetListAction(_ref) {
|
|
|
33
37
|
_getListQuery = getListQuery({
|
|
34
38
|
db: db,
|
|
35
39
|
fields: fields,
|
|
40
|
+
filterable: exposed,
|
|
36
41
|
jsonArrayFields: jsonArrayFields,
|
|
37
42
|
params: other,
|
|
38
43
|
relations: relations,
|
|
@@ -46,7 +51,7 @@ export function createGetListAction(_ref) {
|
|
|
46
51
|
pageSize: pageSize,
|
|
47
52
|
query: query
|
|
48
53
|
})());
|
|
49
|
-
case
|
|
54
|
+
case 7:
|
|
50
55
|
case "end":
|
|
51
56
|
return _context.stop();
|
|
52
57
|
}
|
|
@@ -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;
|
|
@@ -10,14 +10,21 @@ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try
|
|
|
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
11
|
import { rpcOperation } from "next-rest-framework";
|
|
12
12
|
import { listBodySchema } from "../schemas/index.js";
|
|
13
|
+
import { normalizePaging } from "./paging.js";
|
|
13
14
|
export function getListData(_ref) {
|
|
14
15
|
var query = _ref.query,
|
|
15
16
|
countQuery = _ref.countQuery,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
_ref$current = _ref.current,
|
|
19
|
-
current = _ref$current === void 0 ? 1 : _ref$current,
|
|
17
|
+
rawPageSize = _ref.pageSize,
|
|
18
|
+
rawCurrent = _ref.current,
|
|
20
19
|
bodySchema = _ref.bodySchema;
|
|
20
|
+
// 这两个值到这儿还是 query 里来的字符串。归一化的三个理由见 paging.ts —— 最要紧的一条是
|
|
21
|
+
// `+"abc"` 是 NaN,而 drizzle 遇到 falsy 的 limit 是**整条 LIMIT 不加**,一个笔误就是全量返回。
|
|
22
|
+
var _normalizePaging = normalizePaging({
|
|
23
|
+
current: rawCurrent,
|
|
24
|
+
pageSize: rawPageSize
|
|
25
|
+
}),
|
|
26
|
+
current = _normalizePaging.current,
|
|
27
|
+
pageSize = _normalizePaging.pageSize;
|
|
21
28
|
return rpcOperation().outputs([{
|
|
22
29
|
body: listBodySchema(bodySchema),
|
|
23
30
|
contentType: "application/json"
|
|
@@ -27,7 +34,7 @@ export function getListData(_ref) {
|
|
|
27
34
|
while (1) switch (_context.prev = _context.next) {
|
|
28
35
|
case 0:
|
|
29
36
|
_context.next = 2;
|
|
30
|
-
return query.limit(
|
|
37
|
+
return query.limit(pageSize).offset((current - 1) * pageSize).execute();
|
|
31
38
|
case 2:
|
|
32
39
|
data = _context.sent;
|
|
33
40
|
_context.next = 5;
|
|
@@ -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 —— 写成可选的话,漏传就退化成查全表,
|
|
@@ -27,7 +27,8 @@ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) r
|
|
|
27
27
|
// 日期范围怎么认、多值怎么拆、排序与分页怎么加、计数查询怎么建,两边一模一样。**真正的方言
|
|
28
28
|
// 差异只有两条原语**:
|
|
29
29
|
//
|
|
30
|
-
// · 大小写不敏感的模糊匹配 —— pg
|
|
30
|
+
// · 大小写不敏感的模糊匹配 —— pg 用 ilike,sqlite 的 like 本来就不区分大小写,但要自己带
|
|
31
|
+
// ESCAPE(pg 的 LIKE 默认拿反斜杠当转义符,sqlite 不带 ESCAPE 子句就一个转义符都没有);
|
|
31
32
|
// · 「JSON 数组列里有没有含这段文字的元素」—— pg 用 jsonb_array_elements_text,
|
|
32
33
|
// sqlite 用 json_each(而且取值的字段名也不同:tag vs tag.value)。
|
|
33
34
|
//
|
|
@@ -37,8 +38,10 @@ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) r
|
|
|
37
38
|
// 抄两份的代价不是抽象的:这个文件改过好几次(作用域先于筛选、空值跳过、日期范围),
|
|
38
39
|
// 每一次都得记得改另一棵树,而漏掉不会有任何东西报错 —— 只会让 sqlite 那边悄悄少一条规则。
|
|
39
40
|
|
|
40
|
-
import { and, asc, desc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
|
|
41
|
+
import { and, asc, Column, desc, eq, gte, inArray, is, lte, or, sql } from "drizzle-orm";
|
|
42
|
+
import { HttpError } from "../errors.js";
|
|
41
43
|
import { scopeCondition } from "../scope.js";
|
|
44
|
+
import { normalizePaging } from "./paging.js";
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
47
|
* 核心用得到表的哪一部分:只有 `id`(计数与分组要它)和「按列名取列」。
|
|
@@ -47,29 +50,71 @@ import { scopeCondition } from "../scope.js";
|
|
|
47
50
|
|
|
48
51
|
/** 两套方言之间**全部**的差异。加第三种数据库时要写的也只有这两条。 */
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 把用户输入变成 LIKE 模式里的**字面量**。
|
|
55
|
+
*
|
|
56
|
+
* 不转义的话 `?name=%` 就是「匹配全部」、`?name=a_c` 里的下划线是「任意一个字符」—— 前者只是
|
|
57
|
+
* 筛不准,后者配合「能按某一列筛」就是一个逐字符试探值的通道。反斜杠自己也要转,否则
|
|
58
|
+
* `?name=\` 会把后面那个 `%` 吃掉。
|
|
59
|
+
*/
|
|
60
|
+
export function escapeLikePattern(keyword) {
|
|
61
|
+
return "%".concat(keyword.replace(/[\\%_]/g, function (char) {
|
|
62
|
+
return "\\".concat(char);
|
|
63
|
+
}), "%");
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
// 按列名从表(或它的关联表)里取列。**唯一一处按字符串索引的地方** —— 收在这儿,
|
|
51
67
|
// 免得每个用到的地方都写一次断言。取不到回 undefined,调用方据此跳过这个条件。
|
|
68
|
+
//
|
|
69
|
+
// is(…, Column) 这层不是多余的:按字符串取属性会取到原型链上的东西(`?orderBy=constructor`
|
|
70
|
+
// 拿到的是 Object 构造函数),而它不会在这里报错,会一路流到 drizzle 里才炸 —— 症状是 500,
|
|
71
|
+
// 而正确的症状是「这不是一个列」。
|
|
52
72
|
function columnAt(table, key) {
|
|
53
|
-
|
|
73
|
+
var value = table[key];
|
|
74
|
+
return is(value, Column) ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 「这个键该精确匹配还是模糊匹配」。
|
|
78
|
+
//
|
|
79
|
+
// 从前是 `/id/i.test(key)` —— 「名字里带 id」,于是 width / hidden / video 这类列名也被当成 id
|
|
80
|
+
// 列。文档写的一直是「以 Id 结尾」,这里按文档来。
|
|
81
|
+
function isIdKey(key) {
|
|
82
|
+
return key === "id" || key.endsWith("Id");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 模糊匹配只在**文本列**上成立。uuid / 数字 / 时间 / 枚举列上 ILIKE 不是「筛不出东西」,是
|
|
86
|
+
// 数据库层面的类型错误(PG:operator does not exist: uuid ~~* unknown),也就是一个 500。
|
|
87
|
+
// 所以这些列一律退回精确匹配 —— 这同时让上面那条 isIdKey 的收紧变得安全:一个不以 Id 结尾的
|
|
88
|
+
// uuid 列(比如 parent、owner)从「精确」掉到「模糊」也不会炸。
|
|
89
|
+
function isFuzzyMatchable(column) {
|
|
90
|
+
return column.dataType === "string" && !/uuid|enum/i.test(column.columnType);
|
|
54
91
|
}
|
|
55
92
|
export function createGetListQuery(dialect) {
|
|
56
93
|
return function getListQuery(_ref) {
|
|
57
94
|
var db = _ref.db,
|
|
58
95
|
fields = _ref.fields,
|
|
96
|
+
filterable = _ref.filterable,
|
|
59
97
|
jsonArrayFields = _ref.jsonArrayFields,
|
|
60
98
|
params = _ref.params,
|
|
61
99
|
relations = _ref.relations,
|
|
62
100
|
scope = _ref.scope,
|
|
63
101
|
table = _ref.table;
|
|
64
|
-
var
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pageSize = _params$pageSize === void 0 ? 10 : _params$pageSize,
|
|
68
|
-
_params$orderBy = params.orderBy,
|
|
69
|
-
orderBy = _params$orderBy === void 0 ? "createdAt" : _params$orderBy,
|
|
102
|
+
var page = params.page,
|
|
103
|
+
rawPageSize = params.pageSize,
|
|
104
|
+
orderBy = params.orderBy,
|
|
70
105
|
_params$orderDir = params.orderDir,
|
|
71
106
|
orderDir = _params$orderDir === void 0 ? "desc" : _params$orderDir,
|
|
72
107
|
filters = _objectWithoutProperties(params, _excluded);
|
|
108
|
+
var _normalizePaging = normalizePaging({
|
|
109
|
+
current: page,
|
|
110
|
+
pageSize: rawPageSize
|
|
111
|
+
}),
|
|
112
|
+
current = _normalizePaging.current,
|
|
113
|
+
pageSize = _normalizePaging.pageSize;
|
|
114
|
+
var allowed = filterable ? new Set(filterable) : undefined;
|
|
115
|
+
var isFilterable = function isFilterable(key) {
|
|
116
|
+
return !allowed || allowed.has(key);
|
|
117
|
+
};
|
|
73
118
|
|
|
74
119
|
// ==========================================================================
|
|
75
120
|
// 基础查询
|
|
@@ -123,11 +168,11 @@ export function createGetListQuery(dialect) {
|
|
|
123
168
|
// 单个筛选条件
|
|
124
169
|
// ==========================================================================
|
|
125
170
|
// 三条规矩,两套方言一致:
|
|
126
|
-
// ·
|
|
171
|
+
// · 以 Id 结尾的键(以及非文本列)按精确匹配(多值走 IN),其余按模糊;
|
|
127
172
|
// · 逗号分隔视为多值;
|
|
128
173
|
// · JSON 数组列走 dialect 那条 EXISTS。
|
|
129
174
|
function addCondition(conditions, key, value, targetColumn) {
|
|
130
|
-
var
|
|
175
|
+
var isExact = isIdKey(key) || !isFuzzyMatchable(targetColumn);
|
|
131
176
|
var isJsonArray = jsonArrayFields === null || jsonArrayFields === void 0 ? void 0 : jsonArrayFields.includes(key);
|
|
132
177
|
if (typeof value !== "string") {
|
|
133
178
|
conditions.push(eq(targetColumn, value));
|
|
@@ -142,22 +187,22 @@ export function createGetListQuery(dialect) {
|
|
|
142
187
|
}
|
|
143
188
|
if (isJsonArray) {
|
|
144
189
|
// 多值时只认第一个 —— 与从前逐字一致,不趁重构改行为。
|
|
145
|
-
conditions.push(dialect.jsonArrayContains(targetColumn, values[0]));
|
|
146
|
-
} else if (
|
|
190
|
+
conditions.push(dialect.jsonArrayContains(targetColumn, escapeLikePattern(values[0])));
|
|
191
|
+
} else if (isExact) {
|
|
147
192
|
conditions.push(inArray(targetColumn, values));
|
|
148
193
|
} else {
|
|
149
194
|
conditions.push(or.apply(void 0, _toConsumableArray(values.map(function (item) {
|
|
150
|
-
return dialect.contains(targetColumn, item);
|
|
195
|
+
return dialect.contains(targetColumn, escapeLikePattern(item));
|
|
151
196
|
}))));
|
|
152
197
|
}
|
|
153
198
|
return;
|
|
154
199
|
}
|
|
155
200
|
if (isJsonArray) {
|
|
156
|
-
conditions.push(dialect.jsonArrayContains(targetColumn, value));
|
|
157
|
-
} else if (
|
|
201
|
+
conditions.push(dialect.jsonArrayContains(targetColumn, escapeLikePattern(value)));
|
|
202
|
+
} else if (isExact) {
|
|
158
203
|
conditions.push(eq(targetColumn, value));
|
|
159
204
|
} else {
|
|
160
|
-
conditions.push(dialect.contains(targetColumn, value));
|
|
205
|
+
conditions.push(dialect.contains(targetColumn, escapeLikePattern(value)));
|
|
161
206
|
}
|
|
162
207
|
}
|
|
163
208
|
function buildConditions() {
|
|
@@ -173,12 +218,26 @@ export function createGetListQuery(dialect) {
|
|
|
173
218
|
|
|
174
219
|
// 日期范围:xxxAtFrom / xxxAtTo 落到 xxxAt 那一列上。
|
|
175
220
|
if (key.endsWith("AtFrom") || key.endsWith("AtTo")) {
|
|
176
|
-
var baseFieldName = key.replace(/(AtFrom|AtTo)$/, "");
|
|
177
|
-
|
|
221
|
+
var baseFieldName = "".concat(key.replace(/(AtFrom|AtTo)$/, ""), "At");
|
|
222
|
+
// 准入按落到的**那一列**判,不按参数名 —— createdAtFrom 能不能用,取决于
|
|
223
|
+
// createdAt 是不是一个调用方读得到的列。
|
|
224
|
+
if (!isFilterable(baseFieldName)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
var _targetColumn = findTargetColumn(baseFieldName);
|
|
178
228
|
if (!_targetColumn) {
|
|
179
229
|
continue;
|
|
180
230
|
}
|
|
181
|
-
|
|
231
|
+
var at = new Date(value);
|
|
232
|
+
// 认不出来的日期不能往下传:drizzle 序列化 Invalid Date 时抛 RangeError,
|
|
233
|
+
// 那是一个 500,而这明明是请求写错了。
|
|
234
|
+
if (Number.isNaN(at.getTime())) {
|
|
235
|
+
throw new HttpError(400, "".concat(key, " \u4E0D\u662F\u4E00\u4E2A\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\uFF0C\u8BF7\u7528 ISO 8601\uFF08\u5982 2026-08-01T00:00:00Z\uFF09\u3002"));
|
|
236
|
+
}
|
|
237
|
+
conditions.push(key.endsWith("AtFrom") ? gte(_targetColumn, at) : lte(_targetColumn, at));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!isFilterable(key)) {
|
|
182
241
|
continue;
|
|
183
242
|
}
|
|
184
243
|
var targetColumn = findTargetColumn(key);
|
|
@@ -190,7 +249,18 @@ export function createGetListQuery(dialect) {
|
|
|
190
249
|
return conditions;
|
|
191
250
|
}
|
|
192
251
|
function buildOrderBy() {
|
|
193
|
-
|
|
252
|
+
// 请求里给了值、和「谁都没给于是走默认」是两件事,错误也就不是同一种:前者是请求写错
|
|
253
|
+
// (400),后者是这张表上根本没有 createdAt,属于配置错(跟 scopeCondition 那边同一类,
|
|
254
|
+
// 抛普通 Error)。从前两种都退化成 `order by desc` —— 一句语法错误的 SQL,500。
|
|
255
|
+
var requested = typeof orderBy === "string" && orderBy ? orderBy : undefined;
|
|
256
|
+
var orderKey = requested !== null && requested !== void 0 ? requested : "createdAt";
|
|
257
|
+
var orderColumn = columnAt(table, orderKey);
|
|
258
|
+
if (!orderColumn) {
|
|
259
|
+
if (requested) {
|
|
260
|
+
throw new HttpError(400, "orderBy \"".concat(requested, "\" \u4E0D\u662F\u8FD9\u5F20\u8868\u4E0A\u7684\u5217\u3002"));
|
|
261
|
+
}
|
|
262
|
+
throw new Error("\u9ED8\u8BA4\u6392\u5E8F\u5217 createdAt \u4E0D\u5728\u8FD9\u5F20\u8868\u4E0A\u3002\u7ED9\u8FD9\u5F20\u8868\u7684\u5217\u8868\u67E5\u8BE2\u663E\u5F0F\u4F20\u4E00\u4E2A orderBy\u3002");
|
|
263
|
+
}
|
|
194
264
|
return orderDir === "asc" ? asc(orderColumn) : desc(orderColumn);
|
|
195
265
|
}
|
|
196
266
|
|
|
@@ -205,7 +275,7 @@ export function createGetListQuery(dialect) {
|
|
|
205
275
|
var conditions = scopeSql ? [scopeSql] : [];
|
|
206
276
|
conditions.push.apply(conditions, _toConsumableArray(buildConditions()));
|
|
207
277
|
var where = conditions.length > 0 ? and.apply(void 0, conditions) : undefined;
|
|
208
|
-
var query = baseQuery.where(where).orderBy(buildOrderBy()).limit(pageSize).offset((
|
|
278
|
+
var query = baseQuery.where(where).orderBy(buildOrderBy()).limit(pageSize).offset((current - 1) * pageSize);
|
|
209
279
|
var countQuery = db.select({
|
|
210
280
|
count: sql(_templateObject || (_templateObject = _taggedTemplateLiteral(["COUNT(DISTINCT ", ")"])), table.id)
|
|
211
281
|
}).from(table).where(where);
|
|
@@ -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 —— 写成可选的话,漏传就退化成查全表,
|
|
@@ -8,14 +8,17 @@ function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(
|
|
|
8
8
|
|
|
9
9
|
import { ilike, sql } from "drizzle-orm";
|
|
10
10
|
import { createGetListQuery } from "./get-list-query-core.js";
|
|
11
|
+
|
|
12
|
+
// pattern 由核心转义好再传进来(见 escapeLikePattern)。pg 这边不用写 ESCAPE 子句:
|
|
13
|
+
// LIKE / ILIKE 默认就拿反斜杠当转义符,而模式串是绑定参数,不经过字符串字面量那套解析。
|
|
11
14
|
var run = createGetListQuery({
|
|
12
|
-
contains: function contains(column,
|
|
13
|
-
return ilike(column,
|
|
15
|
+
contains: function contains(column, pattern) {
|
|
16
|
+
return ilike(column, pattern);
|
|
14
17
|
},
|
|
15
18
|
// jsonb 数组展开成行再逐个 LIKE。列存的是 text[] 形状的 jsonb(见 createTableSchema 的
|
|
16
19
|
// jsonArrayFields),所以用 _text 那个变体,取到的 tag 直接就是字符串。
|
|
17
|
-
jsonArrayContains: function jsonArrayContains(column,
|
|
18
|
-
return sql(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n EXISTS (\n SELECT 1 FROM jsonb_array_elements_text(", ") tag\n WHERE tag LIKE ", "\n )\n "])), column,
|
|
20
|
+
jsonArrayContains: function jsonArrayContains(column, pattern) {
|
|
21
|
+
return sql(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n EXISTS (\n SELECT 1 FROM jsonb_array_elements_text(", ") tag\n WHERE tag LIKE ", "\n )\n "])), column, pattern);
|
|
19
22
|
}
|
|
20
23
|
});
|
|
21
24
|
export function getListQuery(args) {
|
|
@@ -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";
|
|
@@ -3,4 +3,5 @@ export { createGetAction } from "./create-get-action.js";
|
|
|
3
3
|
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
|
-
export { getListQuery } from "./get-list-query.js";
|
|
6
|
+
export { getListQuery } from "./get-list-query.js";
|
|
7
|
+
export { MAX_PAGE_SIZE, normalizePaging } from "./paging.js";
|
|
@@ -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,32 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// 分页参数归一化(pg / sqlite 共用)
|
|
3
|
+
// ==============================================================================
|
|
4
|
+
// query 参数在线上只有字符串,而这两个值最后是直接进 `.limit()` / `.offset()` 的。不归一化的
|
|
5
|
+
// 后果不是「筛不准」,是三种更难看的症状:
|
|
6
|
+
//
|
|
7
|
+
// · `?pageSize=abc` → `+"abc"` 是 NaN,而 drizzle 对 falsy 的 limit 是**整条 LIMIT 不加** ——
|
|
8
|
+
// 一个笔误就是全量返回。这是这个文件存在的首要理由。
|
|
9
|
+
// · `?current=0` → OFFSET 变成负数,数据库直接报错,一个 500。
|
|
10
|
+
// · `?pageSize=99999999` → 真的去拉这么多行。
|
|
11
|
+
//
|
|
12
|
+
// 上界是**截断**而不是报错:列表的 describe 一直在教调用方「要整批就把 pageSize 调大」,
|
|
13
|
+
// 那些调用方传的是 5000 这种量级的真实值,为了一个上界让它们 400 不值当。截断之后 total 照常
|
|
14
|
+
// 是真实总数,调用方分得清自己有没有拿全。
|
|
15
|
+
|
|
16
|
+
/** pageSize 的硬上界。取这个量级是因为「一次把一整块数据取回来」是列表的正常用法。 */
|
|
17
|
+
export var MAX_PAGE_SIZE = 10000;
|
|
18
|
+
export var DEFAULT_PAGE_SIZE = 10;
|
|
19
|
+
function toPositiveInt(value, fallback) {
|
|
20
|
+
var parsed = Math.floor(Number(value));
|
|
21
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 返回一定能安全喂给 limit / offset 的一对整数。 */
|
|
25
|
+
export function normalizePaging(_ref) {
|
|
26
|
+
var current = _ref.current,
|
|
27
|
+
pageSize = _ref.pageSize;
|
|
28
|
+
return {
|
|
29
|
+
current: toPositiveInt(current, 1),
|
|
30
|
+
pageSize: Math.min(toPositiveInt(pageSize, DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -98,11 +98,18 @@ export function createPostOperationFactory(_ref) {
|
|
|
98
98
|
// creatorId,作者就没人写了,而 setBody 只能返回请求体 schema 里有的字段。
|
|
99
99
|
// 归属戳排在它后面 —— 两者撞同一个键时以归属为准,那是更强的那条不变量。
|
|
100
100
|
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
101
|
+
// **归属戳必须最后展开**,这是一条安全不变量,别为了「让调用方能覆盖」而调顺序。
|
|
102
|
+
//
|
|
103
|
+
// 请求体到这里已经被 `.input({ body })` 过了一遍 zod:next-rest-framework 把 handler
|
|
104
|
+
// 收到的那个 clone 的 `.json()` 换成了校验后的结果,所以 schema 里没有的键(归属列、
|
|
105
|
+
// basicFields、serverColumns)在这一步之前就没了。**但别把那当成这里的边界**:
|
|
106
|
+
// · 它是被钉住的那个 fork 的行为,本仓库拦不住它变(见
|
|
107
|
+
// __tests__/write-body-strip,那组用例就是钉它的);
|
|
108
|
+
// · 自定义 parseBody 会绕开它;`.catchall()` / `.passthrough()` 的 body schema
|
|
109
|
+
// 也会让未知键活着过来;
|
|
110
|
+
// · createPostAction 是 `db.insert(table).values(body)`,中间没有第二道 schema
|
|
111
|
+
// 过滤,而 drizzle 会老老实实写下每一个**是真列**的键。
|
|
112
|
+
// 所以归属这一条不靠 schema,靠顺序:戳排在 body 后面,客户端塞什么都盖不掉。
|
|
106
113
|
// 见 __tests__/route-operation-scope-stamp。
|
|
107
114
|
stamped = session === undefined ? {} : (_access$stamp = access === null || access === void 0 || (_access$stamp2 = access.stamp) === null || _access$stamp2 === void 0 ? void 0 : _access$stamp2.call(access, session)) !== null && _access$stamp !== void 0 ? _access$stamp : {};
|
|
108
115
|
params = _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, body), extraBody), stamped), scopeStamp(scope));
|
|
@@ -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>;
|
|
@@ -66,7 +66,7 @@ export var BASIC_UPDATE_OMIT = {
|
|
|
66
66
|
export var queryListSchema = function queryListSchema(schema) {
|
|
67
67
|
return z.object({
|
|
68
68
|
current: z.string().optional().default("1").describe("Page number, starting at 1."),
|
|
69
|
-
pageSize: z.string().optional().default("10").describe("Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."),
|
|
69
|
+
pageSize: z.string().optional().default("10").describe("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."),
|
|
70
70
|
createdAtFrom: z.string().optional().describe("Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."),
|
|
71
71
|
createdAtTo: z.string().optional().describe("Keep only rows created at or before this instant."),
|
|
72
72
|
orderBy: z.string().optional().describe("Column to sort by — a column name of this resource (createdAt / updatedAt and the like)."),
|