@ai0x0/utils 0.6.0 → 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-delete-action.d.ts +2 -3
- package/es/backend/actions/create-delete-action.js +8 -44
- 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/create-put-action.d.ts +1 -1
- package/es/backend/actions/create-put-action.js +15 -39
- 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 +67 -0
- package/es/backend/actions/get-list-query-core.js +306 -0
- package/es/backend/actions/get-list-query.d.ts +7 -18
- package/es/backend/actions/get-list-query.js +23 -220
- 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/actions/write-action-core.d.ts +18 -0
- package/es/backend/actions/write-action-core.js +119 -0
- package/es/backend/route-operation/write-operation-core.js +12 -5
- package/es/backend/schemas/common.d.ts +112 -0
- package/es/backend/schemas/common.js +158 -0
- package/es/backend/schemas/index.d.ts +735 -364
- package/es/backend/schemas/index.js +8 -139
- package/es/backend/sqlite/actions/create-delete-action.d.ts +1 -2
- package/es/backend/sqlite/actions/create-delete-action.js +7 -43
- 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/create-put-action.d.ts +1 -1
- package/es/backend/sqlite/actions/create-put-action.js +13 -36
- 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 +6 -9
- package/es/backend/sqlite/actions/get-list-query.js +24 -217
- package/es/backend/sqlite/errors.d.ts +1 -4
- package/es/backend/sqlite/errors.js +6 -31
- package/es/backend/sqlite/schemas/index.d.ts +1 -111
- package/es/backend/sqlite/schemas/index.js +8 -139
- package/lib/backend/actions/create-delete-action.d.ts +2 -3
- package/lib/backend/actions/create-delete-action.js +2 -14
- 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/create-put-action.d.ts +1 -1
- package/lib/backend/actions/create-put-action.js +3 -16
- 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 +67 -0
- package/lib/backend/actions/get-list-query-core.js +206 -0
- package/lib/backend/actions/get-list-query.d.ts +7 -18
- package/lib/backend/actions/get-list-query.js +16 -141
- 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/actions/write-action-core.d.ts +18 -0
- package/lib/backend/actions/write-action-core.js +70 -0
- package/lib/backend/schemas/common.d.ts +112 -0
- package/lib/backend/schemas/common.js +112 -0
- package/lib/backend/schemas/index.d.ts +735 -364
- package/lib/backend/schemas/index.js +12 -83
- package/lib/backend/sqlite/actions/create-delete-action.d.ts +1 -2
- package/lib/backend/sqlite/actions/create-delete-action.js +2 -14
- 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/create-put-action.d.ts +1 -1
- package/lib/backend/sqlite/actions/create-put-action.js +3 -16
- 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 +6 -9
- package/lib/backend/sqlite/actions/get-list-query.js +16 -141
- package/lib/backend/sqlite/errors.d.ts +1 -4
- package/lib/backend/sqlite/errors.js +2 -8
- package/lib/backend/sqlite/schemas/index.d.ts +1 -111
- package/lib/backend/sqlite/schemas/index.js +12 -83
- 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.
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { BaseTable } from "../types.js";
|
|
3
|
-
import { ScopeArg } from "../scope.js";
|
|
4
3
|
export declare function createDeleteAction<TTable extends BaseTable>({ table, db, }: {
|
|
5
4
|
db: NodePgDatabase<Record<string, unknown>>;
|
|
6
5
|
table: TTable;
|
|
7
6
|
}): ({ id }: {
|
|
8
7
|
id: string;
|
|
9
8
|
}, { scope }: {
|
|
10
|
-
scope: ScopeArg;
|
|
11
|
-
}) => Promise<
|
|
9
|
+
scope: import("..").ScopeArg;
|
|
10
|
+
}) => Promise<any>;
|
|
@@ -1,48 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { and, eq } from "drizzle-orm";
|
|
6
|
-
import { NO_SCOPE, scopeCondition } from "../scope.js";
|
|
7
|
-
import { HttpError } from "../errors.js";
|
|
1
|
+
// 删一行。实现(含三条安全性质:归属写进 where、作用域必填、不区分「不存在」与「不归你」)
|
|
2
|
+
// 在 write-action-core,与 sqlite 共用。
|
|
3
|
+
|
|
4
|
+
import { createDeleteActionCore } from "./write-action-core.js";
|
|
8
5
|
export function createDeleteAction(_ref) {
|
|
9
6
|
var table = _ref.table,
|
|
10
7
|
db = _ref.db;
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
while (1) switch (_context.prev = _context.next) {
|
|
16
|
-
case 0:
|
|
17
|
-
id = _ref2.id;
|
|
18
|
-
scope = _ref3.scope;
|
|
19
|
-
if (id) {
|
|
20
|
-
_context.next = 4;
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
throw new HttpError(400, "缺少 id");
|
|
24
|
-
case 4:
|
|
25
|
-
// 归属写进 DELETE 的 where,不先查后删(理由同 create-put-action)。
|
|
26
|
-
guard = scopeCondition(table, scope);
|
|
27
|
-
_context.next = 7;
|
|
28
|
-
return db.delete(table).where(guard ? and(eq(table.id, id), guard) : eq(table.id, id)).returning();
|
|
29
|
-
case 7:
|
|
30
|
-
rows = _context.sent;
|
|
31
|
-
if (!(rows.length === 0 && scope !== NO_SCOPE)) {
|
|
32
|
-
_context.next = 10;
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
throw new HttpError(404, "未找到删除对象,或没有权限");
|
|
36
|
-
case 10:
|
|
37
|
-
return _context.abrupt("return", rows);
|
|
38
|
-
case 11:
|
|
39
|
-
case "end":
|
|
40
|
-
return _context.stop();
|
|
41
|
-
}
|
|
42
|
-
}, _callee);
|
|
43
|
-
}));
|
|
44
|
-
return function (_x, _x2) {
|
|
45
|
-
return _ref4.apply(this, arguments);
|
|
46
|
-
};
|
|
47
|
-
}();
|
|
8
|
+
return createDeleteActionCore({
|
|
9
|
+
db: db,
|
|
10
|
+
table: table
|
|
11
|
+
});
|
|
48
12
|
}
|
|
@@ -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
|
}
|
|
@@ -6,6 +6,6 @@ export declare function createPutAction<T extends ZodSchema, TTable extends Base
|
|
|
6
6
|
bodySchema: T;
|
|
7
7
|
db: NodePgDatabase<Record<string, unknown>>;
|
|
8
8
|
table: TTable;
|
|
9
|
-
}): (body: Partial<z.infer<T>> & Record<string, unknown>,
|
|
9
|
+
}): (body: Partial<z.infer<T>> & Record<string, unknown>, options: {
|
|
10
10
|
scope: ScopeArg;
|
|
11
11
|
}) => Promise<z.infer<T>>;
|
|
@@ -1,59 +1,35 @@
|
|
|
1
1
|
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); }
|
|
2
2
|
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; }
|
|
3
|
-
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
4
|
-
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
5
|
-
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
6
|
-
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
|
7
|
-
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
8
|
-
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
9
3
|
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
4
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
5
|
+
// 改一行。实现(含三条安全性质)在 write-action-core,与 sqlite 共用;这里只把签名收紧到
|
|
6
|
+
// pg 的类型上 —— 核心为了同时喂两套方言,db 与返回值那两栏是宽的,调用方不该跟着变宽。
|
|
7
|
+
|
|
8
|
+
import { createPutActionCore } from "./write-action-core.js";
|
|
15
9
|
export function createPutAction(_ref) {
|
|
16
10
|
var db = _ref.db,
|
|
17
11
|
table = _ref.table;
|
|
12
|
+
var run = createPutActionCore({
|
|
13
|
+
db: db,
|
|
14
|
+
table: table
|
|
15
|
+
});
|
|
18
16
|
return /*#__PURE__*/function () {
|
|
19
|
-
var
|
|
20
|
-
var scope, id, guard, _yield$db$update$set$, _yield$db$update$set$2, data;
|
|
17
|
+
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(body, options) {
|
|
21
18
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
22
19
|
while (1) switch (_context.prev = _context.next) {
|
|
23
20
|
case 0:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
throw new HttpError(400, "缺少 id");
|
|
31
|
-
case 4:
|
|
32
|
-
// 归属条件直接写进 UPDATE 的 where,而不是「先查一次确认归属、再无条件更新」。
|
|
33
|
-
// 先查后改有两个问题:多一次往返,以及那一查一改之间归属可能已经变了(转移给团队、
|
|
34
|
-
// 踢出成员)。一条语句里带上归属,改不到就是改不到。
|
|
35
|
-
guard = scopeCondition(table, scope);
|
|
36
|
-
_context.next = 7;
|
|
37
|
-
return db.update(table).set(transformBody(body)).where(guard ? and(eq(table.id, id), guard) : eq(table.id, id)).returning();
|
|
38
|
-
case 7:
|
|
39
|
-
_yield$db$update$set$ = _context.sent;
|
|
40
|
-
_yield$db$update$set$2 = _slicedToArray(_yield$db$update$set$, 1);
|
|
41
|
-
data = _yield$db$update$set$2[0];
|
|
42
|
-
if (!(!data && scope !== NO_SCOPE)) {
|
|
43
|
-
_context.next = 12;
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
throw new HttpError(404, "未找到编辑对象,或没有权限");
|
|
47
|
-
case 12:
|
|
48
|
-
return _context.abrupt("return", data);
|
|
49
|
-
case 13:
|
|
21
|
+
_context.next = 2;
|
|
22
|
+
return run(body, options);
|
|
23
|
+
case 2:
|
|
24
|
+
return _context.abrupt("return", _context.sent);
|
|
25
|
+
case 3:
|
|
50
26
|
case "end":
|
|
51
27
|
return _context.stop();
|
|
52
28
|
}
|
|
53
29
|
}, _callee);
|
|
54
30
|
}));
|
|
55
31
|
return function (_x, _x2) {
|
|
56
|
-
return
|
|
32
|
+
return _ref2.apply(this, arguments);
|
|
57
33
|
};
|
|
58
34
|
}();
|
|
59
35
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Column, SQL } from "drizzle-orm";
|
|
2
|
+
import { ScopeArg } from "../scope.js";
|
|
3
|
+
/**
|
|
4
|
+
* 核心用得到表的哪一部分:只有 `id`(计数与分组要它)和「按列名取列」。
|
|
5
|
+
* 两棵树的 BaseTable 都满足它,而 dialect 各自的 Table 类型不必进来。
|
|
6
|
+
*/
|
|
7
|
+
export interface ListQueryTable {
|
|
8
|
+
id: Column;
|
|
9
|
+
}
|
|
10
|
+
export interface ListQueryRelation {
|
|
11
|
+
groupBy?: boolean;
|
|
12
|
+
select?: Record<string, unknown>;
|
|
13
|
+
sql?: SQL;
|
|
14
|
+
table?: ListQueryTable;
|
|
15
|
+
}
|
|
16
|
+
/** 两套方言之间**全部**的差异。加第三种数据库时要写的也只有这两条。 */
|
|
17
|
+
export interface ListQueryDialect {
|
|
18
|
+
/**
|
|
19
|
+
* 大小写不敏感的「包含」。收到的 pattern 是**已经转义、已经带好 `%` 的完整模式串** ——
|
|
20
|
+
* 转义收在核心里做(见 escapeLikePattern),免得加第三种方言时漏掉那一步:漏掉不会报错,
|
|
21
|
+
* 只会让 `%` 重新变回通配符。
|
|
22
|
+
*/
|
|
23
|
+
contains(_column: Column, _pattern: string): SQL;
|
|
24
|
+
/** 「这个 JSON 数组列里有没有元素匹配这个模式」。pattern 同上,已转义。 */
|
|
25
|
+
jsonArrayContains(_column: Column, _pattern: string): SQL;
|
|
26
|
+
}
|
|
27
|
+
export interface ListQueryParams {
|
|
28
|
+
[field: string]: unknown;
|
|
29
|
+
orderBy?: string;
|
|
30
|
+
orderDir?: "asc" | "desc";
|
|
31
|
+
page?: number;
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
}
|
|
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, }: {
|
|
43
|
+
db: any;
|
|
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;
|
|
54
|
+
jsonArrayFields?: string[] | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* 行级作用域。**必填**,不想隔离就显式传 NO_SCOPE —— 写成可选的话,漏传就退化成查全表,
|
|
57
|
+
* 而那是静默的越权(见 scope.ts)。它不走下面 params 那条筛选通道,因为那条的规矩是
|
|
58
|
+
* 「空值跳过」,一个取不到值的作用域会让整条 where 消失。
|
|
59
|
+
*/
|
|
60
|
+
scope: ScopeArg;
|
|
61
|
+
params: ListQueryParams;
|
|
62
|
+
relations?: ListQueryRelation[] | undefined;
|
|
63
|
+
table: TTable;
|
|
64
|
+
}) => {
|
|
65
|
+
countQuery: any;
|
|
66
|
+
query: any;
|
|
67
|
+
};
|