@ai0x0/utils 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +336 -0
- package/es/backend/actions/create-delete-action.d.ts +3 -4
- package/es/backend/actions/create-get-action.d.ts +2 -3
- package/es/backend/actions/create-get-list-action.d.ts +2 -3
- package/es/backend/actions/create-post-action.d.ts +4 -4
- package/es/backend/actions/create-put-action.d.ts +2 -3
- package/es/backend/actions/get-list-query.d.ts +4 -12
- package/es/backend/actions/get-list-query.js +11 -8
- package/es/backend/route-operation/delete-operation.d.ts +2 -3
- package/es/backend/route-operation/get-list-operation.d.ts +2 -3
- package/es/backend/route-operation/get-operation.d.ts +2 -3
- package/es/backend/route-operation/get-table-name.d.ts +2 -2
- package/es/backend/route-operation/post-operation.d.ts +5 -5
- package/es/backend/route-operation/put-operation.d.ts +4 -5
- package/es/backend/schemas/index.d.ts +598 -597
- package/es/backend/schemas/index.js +19 -31
- package/es/backend/types.d.ts +14 -8
- package/lib/backend/actions/create-delete-action.d.ts +3 -4
- package/lib/backend/actions/create-get-action.d.ts +2 -3
- package/lib/backend/actions/create-get-list-action.d.ts +2 -3
- package/lib/backend/actions/create-post-action.d.ts +4 -4
- package/lib/backend/actions/create-post-action.js +1 -4
- package/lib/backend/actions/create-put-action.d.ts +2 -3
- package/lib/backend/actions/get-list-query.d.ts +4 -12
- package/lib/backend/actions/get-list-query.js +7 -6
- package/lib/backend/route-operation/delete-operation.d.ts +2 -3
- package/lib/backend/route-operation/get-list-operation.d.ts +2 -3
- package/lib/backend/route-operation/get-operation.d.ts +2 -3
- package/lib/backend/route-operation/get-table-name.d.ts +2 -2
- package/lib/backend/route-operation/post-operation.d.ts +5 -5
- package/lib/backend/route-operation/put-operation.d.ts +4 -5
- package/lib/backend/schemas/index.d.ts +598 -597
- package/lib/backend/schemas/index.js +21 -21
- package/lib/backend/types.d.ts +14 -8
- package/package.json +3 -2
package/SKILL.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai0x0-utils-backend
|
|
3
|
+
description: Scaffold Next.js App Router CRUD endpoints and drizzle-orm(pg) tables with `@ai0x0/utils`. Use when a project needs standard list/get/create/update/delete routes backed by drizzle-orm + zod + next-rest-framework, auto-injected base fields (id / creatorId / createdAt…), paginated list queries with joins, or ready-to-use select/insert/update Zod schemas. Pair with the `create-next-rest-framework-api` skill — this skill replaces hand-written `routeOperation(...).input(...).outputs(...).handler(...)` chains with a declarative factory API exposed inside `next-rest-framework`'s `route({...})`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @ai0x0/utils Backend
|
|
7
|
+
|
|
8
|
+
Declarative CRUD factory on top of `next-rest-framework` v6, `drizzle-orm/pg-core`, `drizzle-zod`, and `zod` v4. Each factory returns a `RouteOperationDefinition<Method>` so it plugs directly into `route({ … })` from next-rest-framework.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Create a shared options module (once per project) that pre-binds `{ db, getSession }` to every operation factory (see "Project Setup" below). Route files never import `db`/`getSession` directly.
|
|
13
|
+
2. For each domain entity, place a `table.ts` next to an `index.ts`:
|
|
14
|
+
- `table.ts` — call `createTableSchema({ name, columns })` and re-export `table` only.
|
|
15
|
+
- `index.ts` — compose the zod schemas (`insertXxxSchema`, `selectXxxSchema`, `updateXxxSchema`, `queryXxxSchema`, `queryListXxxSchema`, `queryListSelectXxxSchema`). Extend with `.merge(z.object(...))` when the business exposes non-column fields (tags, computed columns).
|
|
16
|
+
3. Author the CRUD `route.ts` by wrapping the pre-bound operations inside next-rest-framework's `route({...})`. Name each operation key descriptively — it is the OpenAPI `operationId`.
|
|
17
|
+
4. Drop down to raw `routeOperation(...)` (via the `create-next-rest-framework-api` skill) when the endpoint is non-CRUD (auth, webhooks, streaming, multi-status) or needs custom `TypedNextResponse` unions.
|
|
18
|
+
5. Reuse action-level helpers (`createGetAction`, `createGetListAction`, etc.) inside server actions, cron jobs, queue consumers, or the raw-`routeOperation` handlers — same API, no request wrapping.
|
|
19
|
+
|
|
20
|
+
## Project Setup
|
|
21
|
+
|
|
22
|
+
Create three files once and reuse everywhere:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// app/(backend)/db/schemas/_helper.ts — re-export + lock import path
|
|
26
|
+
export {
|
|
27
|
+
createTableSchema,
|
|
28
|
+
queryListSchema,
|
|
29
|
+
listBodySchema,
|
|
30
|
+
} from "@ai0x0/utils/lib/backend/schemas";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app/(backend)/utils/actions/_helper.ts — action-level helpers
|
|
35
|
+
export {
|
|
36
|
+
createDeleteAction,
|
|
37
|
+
createGetAction,
|
|
38
|
+
createGetListAction,
|
|
39
|
+
createPostAction,
|
|
40
|
+
createPutAction,
|
|
41
|
+
} from "@ai0x0/utils/lib/backend/actions";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// app/(backend)/utils/route-operation/index.ts — pre-bind db + getSession
|
|
46
|
+
import {
|
|
47
|
+
createGetOperation,
|
|
48
|
+
createPutOperation,
|
|
49
|
+
createPostOperation,
|
|
50
|
+
createDeleteOperation,
|
|
51
|
+
createGetListOperation,
|
|
52
|
+
} from "@ai0x0/utils/lib/backend";
|
|
53
|
+
import { db } from "@backend/db";
|
|
54
|
+
import getSession from "@backend/utils/get-session";
|
|
55
|
+
|
|
56
|
+
export const postOperation = createPostOperation({ db, getSession });
|
|
57
|
+
export const deleteOperation = createDeleteOperation({ db, getSession });
|
|
58
|
+
export const putOperation = createPutOperation({ db, getSession });
|
|
59
|
+
export const getOperation = createGetOperation({ db, getSession });
|
|
60
|
+
export const getListOperation = createGetListOperation({ db, getSession });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`getSession(req)` **must** return `{ userId?: string } | undefined` (a Promise is fine). Throwing rejects the request; returning `undefined` disables ownership checks for that call.
|
|
64
|
+
|
|
65
|
+
## Factory Catalog
|
|
66
|
+
|
|
67
|
+
| Export | Purpose | Required options | Notable optional options |
|
|
68
|
+
| ------------------- | -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `createTableSchema` | pg `Table` + 5 zod schemas | `name`, `columns` | `refineSchema`, `extraConfig` |
|
|
70
|
+
| `getOperation` | GET one by filters | `table`, `bodySchema`, `querySchema` | `setParams`, `relations`, `jsonArrayFields`, `byCreator`, `onSuccess`, `onError`, `summary` |
|
|
71
|
+
| `getListOperation` | GET paginated list | `table`, `bodySchema`, `querySchema` | `setParams`, `relations`, `jsonArrayFields`, `onSuccess`, `onError`, `summary` |
|
|
72
|
+
| `postOperation` | POST create | `table`, `bodySchema` | `outputBodySchema`, `setBody`, `onSuccess`, `onError`, `summary` |
|
|
73
|
+
| `putOperation` | PUT update | `table`, `bodySchema` | `outputBodySchema`, `setBody`, `byCreator`, `onSuccess`, `onError`, `summary` |
|
|
74
|
+
| `deleteOperation` | DELETE | `table` | `byCreator`, `onSuccess`, `onError`, `summary` |
|
|
75
|
+
|
|
76
|
+
## Canonical Examples
|
|
77
|
+
|
|
78
|
+
### 1. Table + schemas (`table.ts` + `index.ts`)
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// app/(backend)/db/schemas/agent/table.ts
|
|
82
|
+
import { jsonb, text } from "drizzle-orm/pg-core";
|
|
83
|
+
import { createTableSchema } from "@backend/db/schemas/_helper";
|
|
84
|
+
|
|
85
|
+
export const agentColumns = {
|
|
86
|
+
name: text("name"),
|
|
87
|
+
modelId: text("model_id"),
|
|
88
|
+
tags: jsonb("tags"),
|
|
89
|
+
mcpIds: jsonb("mcp_ids"),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const { table: agents } = createTableSchema({
|
|
93
|
+
name: "agents",
|
|
94
|
+
columns: agentColumns,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// app/(backend)/db/schemas/agent/index.ts
|
|
100
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
101
|
+
import { z } from "zod";
|
|
102
|
+
import { queryListSchema } from "@backend/db/schemas";
|
|
103
|
+
import { agents } from "./table";
|
|
104
|
+
|
|
105
|
+
export { agents, agentColumns } from "./table";
|
|
106
|
+
|
|
107
|
+
export const insertAgentSchema = createInsertSchema(agents).merge(
|
|
108
|
+
z.object({
|
|
109
|
+
mcpIds: z.array(z.string()).optional(),
|
|
110
|
+
tags: z.array(z.string()).optional(),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
export const selectAgentSchema = createSelectSchema(agents).merge(
|
|
114
|
+
z.object({
|
|
115
|
+
mcpIds: z.array(z.string()).optional(),
|
|
116
|
+
tags: z.array(z.string()).optional(),
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
export const updateAgentSchema = insertAgentSchema.required({ id: true });
|
|
120
|
+
export const queryAgentSchema = insertAgentSchema.partial();
|
|
121
|
+
export const queryListAgentSchema = queryListSchema(
|
|
122
|
+
selectAgentSchema.partial().merge(
|
|
123
|
+
z.object({ tags: z.string().optional() }), // 列表里 jsonArray 以逗号分隔字符串传入
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
export const queryListSelectAgentSchema = selectAgentSchema;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Note the two-layer shape: `queryListSchema(...)` input is the **partial of the select schema** (optionally merged with extra querystring-only fields); `bodySchema` in `getListOperation` stays as the full select schema (`queryListSelectAgentSchema`).
|
|
130
|
+
|
|
131
|
+
### 2. Detail CRUD route
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// app/(backend)/api/agent/route.ts
|
|
135
|
+
import { route } from "next-rest-framework";
|
|
136
|
+
import {
|
|
137
|
+
agents,
|
|
138
|
+
insertAgentSchema,
|
|
139
|
+
queryAgentSchema,
|
|
140
|
+
selectAgentSchema,
|
|
141
|
+
updateAgentSchema,
|
|
142
|
+
} from "@backend/db/schemas";
|
|
143
|
+
import {
|
|
144
|
+
deleteOperation,
|
|
145
|
+
getOperation,
|
|
146
|
+
postOperation,
|
|
147
|
+
putOperation,
|
|
148
|
+
} from "@backend/utils/route-operation";
|
|
149
|
+
import getSession from "@backend/utils/get-session";
|
|
150
|
+
import { seedAgentData } from "@backend/utils/actions";
|
|
151
|
+
|
|
152
|
+
export const { POST, GET, DELETE, PUT } = route({
|
|
153
|
+
getAgent: getOperation({
|
|
154
|
+
querySchema: queryAgentSchema,
|
|
155
|
+
bodySchema: selectAgentSchema,
|
|
156
|
+
table: agents,
|
|
157
|
+
summary: "助手详情",
|
|
158
|
+
setParams: async (req) => {
|
|
159
|
+
const { userId } = (await getSession(req)) || {};
|
|
160
|
+
await seedAgentData(userId as string);
|
|
161
|
+
return {};
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
postAgent: postOperation({
|
|
165
|
+
bodySchema: insertAgentSchema,
|
|
166
|
+
table: agents,
|
|
167
|
+
summary: "添加助手",
|
|
168
|
+
setBody: async () => ({ tags: ["my"] }),
|
|
169
|
+
}),
|
|
170
|
+
putAgent: putOperation({
|
|
171
|
+
bodySchema: updateAgentSchema,
|
|
172
|
+
table: agents,
|
|
173
|
+
summary: "编辑助手",
|
|
174
|
+
}),
|
|
175
|
+
deleteAgent: deleteOperation({
|
|
176
|
+
table: agents,
|
|
177
|
+
summary: "删除助手",
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. List route with relations + `onSuccess` tree building
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// app/(backend)/api/channel/list/route.ts
|
|
186
|
+
import { route } from "next-rest-framework";
|
|
187
|
+
import { eq, sql } from "drizzle-orm";
|
|
188
|
+
import {
|
|
189
|
+
channels,
|
|
190
|
+
models,
|
|
191
|
+
queryListChannelSchema,
|
|
192
|
+
queryListSelectChannelSchema,
|
|
193
|
+
} from "@backend/db/schemas";
|
|
194
|
+
import { getListOperation } from "@backend/utils/route-operation";
|
|
195
|
+
import getSession from "@backend/utils/get-session";
|
|
196
|
+
import { seedChannelData } from "@backend/utils/actions/channel";
|
|
197
|
+
|
|
198
|
+
export const { GET } = route({
|
|
199
|
+
getChannelList: getListOperation({
|
|
200
|
+
querySchema: queryListChannelSchema,
|
|
201
|
+
bodySchema: queryListSelectChannelSchema,
|
|
202
|
+
table: channels,
|
|
203
|
+
summary: "获取渠道列表",
|
|
204
|
+
relations: [
|
|
205
|
+
{
|
|
206
|
+
table: models,
|
|
207
|
+
sql: eq(models.channelId, channels.id),
|
|
208
|
+
select: {
|
|
209
|
+
models: sql`(
|
|
210
|
+
SELECT array_agg(DISTINCT jsonb_strip_nulls(to_jsonb(models.*)))
|
|
211
|
+
FROM ${models}
|
|
212
|
+
WHERE ${models.channelId} = ${channels.id}
|
|
213
|
+
)`,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
setParams: async (req) => {
|
|
218
|
+
const { userId } = (await getSession(req)) || {};
|
|
219
|
+
await seedChannelData(userId as string);
|
|
220
|
+
return {};
|
|
221
|
+
},
|
|
222
|
+
onSuccess: async (result) => {
|
|
223
|
+
// 内部字段过滤 / 映射 / 树化都写在这里
|
|
224
|
+
return result;
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For list routes that consume `jsonb` arrays as comma-separated querystring values, add `jsonArrayFields: ["tags"]`.
|
|
231
|
+
|
|
232
|
+
### 4. Escape hatch — drop to raw `routeOperation` but reuse actions
|
|
233
|
+
|
|
234
|
+
When the endpoint returns multiple status shapes (e.g. 200 success + 401 unauthorized), do **not** use the CRUD factories. Call `routeOperation` directly and reuse the action helpers for the database side:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// app/(backend)/api/user/me/route.ts
|
|
238
|
+
import { TypedNextResponse, route, routeOperation } from "next-rest-framework";
|
|
239
|
+
import { eq } from "drizzle-orm";
|
|
240
|
+
import { z } from "zod";
|
|
241
|
+
import { db } from "@backend/db";
|
|
242
|
+
import { members, selectUserSchema, teams, users } from "@backend/db/schemas";
|
|
243
|
+
import { createGetAction } from "@backend/utils/actions";
|
|
244
|
+
import getSession from "@backend/utils/get-session";
|
|
245
|
+
|
|
246
|
+
export const { GET } = route({
|
|
247
|
+
getUserMe: routeOperation({
|
|
248
|
+
method: "GET",
|
|
249
|
+
openApiOperation: { tags: ["users"], summary: "我的信息" },
|
|
250
|
+
})
|
|
251
|
+
.outputs([
|
|
252
|
+
{ status: 200, contentType: "application/json", body: selectUserSchema },
|
|
253
|
+
{
|
|
254
|
+
status: 401,
|
|
255
|
+
contentType: "application/json",
|
|
256
|
+
body: z.object({ message: z.string() }),
|
|
257
|
+
},
|
|
258
|
+
])
|
|
259
|
+
.handler(async (req) => {
|
|
260
|
+
const { userId } = (await getSession(req)) || {};
|
|
261
|
+
const user = await createGetAction({
|
|
262
|
+
db,
|
|
263
|
+
table: users,
|
|
264
|
+
bodySchema: selectUserSchema,
|
|
265
|
+
relations: [
|
|
266
|
+
{ table: members, sql: eq(members.userId, users.id) },
|
|
267
|
+
{
|
|
268
|
+
groupBy: true,
|
|
269
|
+
table: teams,
|
|
270
|
+
select: { team: teams },
|
|
271
|
+
sql: eq(teams.id, members.teamId),
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
})({ id: userId });
|
|
275
|
+
if (!user) {
|
|
276
|
+
return TypedNextResponse.json(
|
|
277
|
+
{ message: "用户不存在" },
|
|
278
|
+
{ status: 401 },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return TypedNextResponse.json(user, { status: 200 });
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Built-in Conventions
|
|
287
|
+
|
|
288
|
+
- **Base fields** auto-added by `createTableSchema`: `id` (uuid, pk, default random) / `creatorId` / `editorId` / `accessedAt` / `createdAt` / `updatedAt`.
|
|
289
|
+
- **Ownership check**: with `byCreator: true` (default on PUT/DELETE/GET), PUT/DELETE pre-fetch the row filtered by `creatorId = session.userId` and throw `"未找到…对象,或没有权限"` if missing. Set `byCreator: false` to skip (e.g. admin endpoints).
|
|
290
|
+
- **Auto injection**: POST writes `creatorId = session.userId`; PUT writes `editorId = session.userId`; GET/GET_LIST append `creatorId = session.userId` to the filter when `byCreator` is on.
|
|
291
|
+
- **Date coercion**: any body field ending in `At` is passed through `dayjs(value).toDate()` before write — accept ISO strings from clients.
|
|
292
|
+
- **List filtering** (all querystring values are strings):
|
|
293
|
+
- `key=a,b,c` → `IN (...)` when key ends with `Id`; otherwise `OR ILIKE %a% OR ILIKE %b%`
|
|
294
|
+
- `key=foo` where key ends with `Id` → `eq`; otherwise `ILIKE %foo%`
|
|
295
|
+
- `${anything}AtFrom` / `${anything}AtTo` → `gte` / `lte` against the matching `${anything}At` column
|
|
296
|
+
- `jsonArrayFields: ["tags"]` → `EXISTS (SELECT 1 FROM jsonb_array_elements_text(col) t WHERE t LIKE %v%)`
|
|
297
|
+
- **Pagination / sort**: `current` (default `"1"`), `pageSize` (default `"10"`), `orderBy` (default `createdAt`), `orderDir` (`asc|desc`, default `desc`).
|
|
298
|
+
- **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.
|
|
299
|
+
|
|
300
|
+
## Guardrails
|
|
301
|
+
|
|
302
|
+
- Always pass `insertXxxSchema` to POST, `updateXxxSchema` (has `.required({ id: true })`) to PUT, `selectXxxSchema` to GET body, `queryListSchema(selectXxxSchema.partial())` to GET_LIST query.
|
|
303
|
+
- Query-string schemas stay `z.string()` — numbers / arrays from querystrings must stay strings and be parsed inside `onSuccess` or `setParams`.
|
|
304
|
+
- Do not reuse `insertSchema` for PUT — missing `id` silently dispatches an update without a where clause.
|
|
305
|
+
- `bodySchema` on list/get is the **response item** schema, not the query shape. Query shape goes in `querySchema`.
|
|
306
|
+
- 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.
|
|
307
|
+
- `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.
|
|
308
|
+
- Swallowing errors: an unhandled throw in a handler propagates to next-rest-framework as a 500. Provide `onError` for any operation exposed to untrusted clients.
|
|
309
|
+
- For non-CRUD behavior (middleware chains, streaming, multipart, `TypedNextResponse` unions, RPC) stop using these factories and switch to the `create-next-rest-framework-api` skill.
|
|
310
|
+
|
|
311
|
+
## Escape Hatches
|
|
312
|
+
|
|
313
|
+
- Extra server-side filters — `setParams(req) => Promise<Record<string, unknown>>` on GET/GET_LIST; returned keys are merged into the filter.
|
|
314
|
+
- Extra columns on write — `setBody(req) => Promise<Partial<infer<IB>>>` on POST/PUT; returned fields are merged into the body before validation runs against `bodySchema`.
|
|
315
|
+
- Post-processing output — `onSuccess(data) => Promise<data>`; on GET_LIST the payload is `{ data, total }`.
|
|
316
|
+
- Error rewriting — `onError(err) => Promise<Response | undefined>`. Return `undefined` to rethrow.
|
|
317
|
+
|
|
318
|
+
## Imports
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
@ai0x0/utils/lib/backend // actions + route-operation + schemas (CJS, used by Next.js server bundle)
|
|
322
|
+
@ai0x0/utils/lib/backend/actions
|
|
323
|
+
@ai0x0/utils/lib/backend/route-operation
|
|
324
|
+
@ai0x0/utils/lib/backend/schemas
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
ESM consumers can substitute `/es/backend/...` instead of `/lib/backend/...` when Next.js resolves the `exports` map that way; prefer `/lib/backend` in Next.js projects because the server bundle is CommonJS.
|
|
328
|
+
|
|
329
|
+
## Done Criteria
|
|
330
|
+
|
|
331
|
+
- Route files contain zero hand-written `{ db, getSession }` wiring; all operations come from `@backend/utils/route-operation`.
|
|
332
|
+
- Each domain has a `table.ts` (columns + `createTableSchema`) and an `index.ts` (five zod schemas + re-exports).
|
|
333
|
+
- Every list endpoint accepts `current / pageSize / orderBy / orderDir / *AtFrom / *AtTo` and — where applicable — `jsonArrayFields` is declared.
|
|
334
|
+
- `byCreator` is explicitly set when the endpoint is admin-facing or otherwise crosses ownership boundaries.
|
|
335
|
+
- Non-CRUD endpoints are implemented via raw `routeOperation` + action helpers, not by forcing a CRUD factory.
|
|
336
|
+
- `next-rest-framework validate` / `generate` (from the partner skill) still pass — the factory values are plain `RouteOperationDefinition` and participate in OpenAPI generation.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BaseTable } from "../types";
|
|
1
|
+
import { BaseTable, AnyDatabase } from "../types";
|
|
3
2
|
export declare function createDeleteAction<TTable extends BaseTable>({ table, db, }: {
|
|
4
|
-
db:
|
|
3
|
+
db: AnyDatabase;
|
|
5
4
|
table: TTable;
|
|
6
5
|
}): ({ id, creatorId }: {
|
|
7
6
|
id: string;
|
|
8
7
|
creatorId?: string | undefined;
|
|
9
|
-
}) => Promise<
|
|
8
|
+
}) => Promise<any>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import { BaseTable, GetListRelations } from "../types";
|
|
2
|
+
import { BaseTable, GetListRelations, AnyDatabase } from "../types";
|
|
4
3
|
export declare function createGetAction<T extends ZodSchema, TTable extends BaseTable>({ bodySchema, db, jsonArrayFields, relations, table, }: {
|
|
5
4
|
bodySchema: T;
|
|
6
|
-
db:
|
|
5
|
+
db: AnyDatabase;
|
|
7
6
|
jsonArrayFields?: string[];
|
|
8
7
|
relations?: GetListRelations;
|
|
9
8
|
table: TTable;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import { BaseTable, GetListRelations } from "../types";
|
|
3
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { BaseTable, GetListRelations, AnyDatabase } from "../types";
|
|
4
3
|
export declare function createGetListAction<T extends ZodSchema, TTable extends BaseTable>({ bodySchema, db, jsonArrayFields, relations, table, }: {
|
|
5
4
|
bodySchema: T;
|
|
6
|
-
db:
|
|
5
|
+
db: AnyDatabase;
|
|
7
6
|
jsonArrayFields?: string[];
|
|
8
7
|
relations?: GetListRelations;
|
|
9
8
|
table: TTable;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export declare function createPostAction<T extends ZodSchema, TTable extends
|
|
2
|
+
import { AnyDatabase } from "../types";
|
|
3
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
4
|
+
export declare function createPostAction<T extends ZodSchema, TTable extends SQLiteTable>({ table, db }: {
|
|
5
5
|
bodySchema: T;
|
|
6
|
-
db:
|
|
6
|
+
db: AnyDatabase;
|
|
7
7
|
table: TTable;
|
|
8
8
|
}): (body: z.infer<T>) => Promise<z.infer<T>[]>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import { BaseTable } from "../types";
|
|
2
|
+
import { BaseTable, AnyDatabase } from "../types";
|
|
4
3
|
export declare function createPutAction<T extends ZodSchema, TTable extends BaseTable>({ db, table, }: {
|
|
5
4
|
bodySchema: T;
|
|
6
|
-
db:
|
|
5
|
+
db: AnyDatabase;
|
|
7
6
|
table: TTable;
|
|
8
7
|
}): (body: Partial<z.infer<T>> & Record<string, unknown>, { byCreator, }?: {
|
|
9
8
|
byCreator?: boolean | undefined;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { BaseTable, GetListRelations } from "../types";
|
|
2
|
-
import { SelectedFields } from "drizzle-orm/
|
|
3
|
-
import { SQL } from "drizzle-orm";
|
|
4
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
1
|
+
import { BaseTable, GetListRelations, AnyDatabase } from "../types";
|
|
2
|
+
import { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types";
|
|
5
3
|
export declare function getListQuery<TTable extends BaseTable, TSelection extends SelectedFields>({ db, fields, jsonArrayFields, params, relations, table, }: {
|
|
6
|
-
db:
|
|
4
|
+
db: AnyDatabase;
|
|
7
5
|
fields: TSelection;
|
|
8
6
|
jsonArrayFields?: string[];
|
|
9
7
|
params: {
|
|
@@ -16,12 +14,6 @@ export declare function getListQuery<TTable extends BaseTable, TSelection extend
|
|
|
16
14
|
relations?: GetListRelations;
|
|
17
15
|
table: TTable;
|
|
18
16
|
}): {
|
|
19
|
-
countQuery:
|
|
20
|
-
count: SQL<unknown>;
|
|
21
|
-
}, "partial", Record<string, "not-null">, false, "where", {
|
|
22
|
-
count: unknown;
|
|
23
|
-
}[], {
|
|
24
|
-
count: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
|
|
25
|
-
}>, "where">;
|
|
17
|
+
countQuery: any;
|
|
26
18
|
query: any;
|
|
27
19
|
};
|
|
@@ -1,11 +1,10 @@
|
|
|
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
|
-
var _templateObject, _templateObject2, _templateObject3;
|
|
3
2
|
var _excluded = ["page", "pageSize", "orderBy", "orderDir"];
|
|
3
|
+
var _templateObject, _templateObject2, _templateObject3, _templateObject4;
|
|
4
4
|
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
5
5
|
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."); }
|
|
6
6
|
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; } }
|
|
7
7
|
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
8
|
-
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
|
|
9
8
|
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
|
|
10
9
|
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
11
10
|
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
|
|
@@ -20,7 +19,11 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
20
19
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
21
20
|
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
|
22
21
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
23
|
-
|
|
22
|
+
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
|
|
23
|
+
import { and, asc, desc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
|
|
24
|
+
var ciLike = function ciLike(col, value) {
|
|
25
|
+
return sql(_templateObject || (_templateObject = _taggedTemplateLiteral(["LOWER(", ") LIKE ", ""])), col, value.toLowerCase());
|
|
26
|
+
};
|
|
24
27
|
export function getListQuery(_ref) {
|
|
25
28
|
var db = _ref.db,
|
|
26
29
|
fields = _ref.fields,
|
|
@@ -102,12 +105,12 @@ export function getListQuery(_ref) {
|
|
|
102
105
|
if (values.length > 0) {
|
|
103
106
|
if (isJsonArray) {
|
|
104
107
|
// 处理JSONB数组字段
|
|
105
|
-
conditions.push(sql(
|
|
108
|
+
conditions.push(sql(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n EXISTS (\n SELECT 1 FROM json_each(", ")\n WHERE LOWER(json_each.value) LIKE ", "\n )\n "])), targetColumn, "%".concat(values[0], "%")));
|
|
106
109
|
} else if (isIdField) {
|
|
107
110
|
conditions.push(inArray(targetColumn, values));
|
|
108
111
|
} else {
|
|
109
112
|
conditions.push(or.apply(void 0, _toConsumableArray(values.map(function (v) {
|
|
110
|
-
return
|
|
113
|
+
return ciLike(targetColumn, "%".concat(v, "%"));
|
|
111
114
|
}))));
|
|
112
115
|
}
|
|
113
116
|
}
|
|
@@ -115,11 +118,11 @@ export function getListQuery(_ref) {
|
|
|
115
118
|
// 处理单值
|
|
116
119
|
if (isJsonArray) {
|
|
117
120
|
// 处理JSONB数组字段
|
|
118
|
-
conditions.push(sql(
|
|
121
|
+
conditions.push(sql(_templateObject3 || (_templateObject3 = _taggedTemplateLiteral(["\n EXISTS (\n SELECT 1 FROM json_each(", ")\n WHERE LOWER(json_each.value) LIKE ", "\n )\n "])), targetColumn, "%".concat(value, "%")));
|
|
119
122
|
} else if (isIdField) {
|
|
120
123
|
conditions.push(eq(targetColumn, value));
|
|
121
124
|
} else {
|
|
122
|
-
conditions.push(
|
|
125
|
+
conditions.push(ciLike(targetColumn, "%".concat(value, "%")));
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
} else {
|
|
@@ -179,7 +182,7 @@ export function getListQuery(_ref) {
|
|
|
179
182
|
|
|
180
183
|
// 构建计数查询(优化后)
|
|
181
184
|
var countQuery = db.select({
|
|
182
|
-
count: sql(
|
|
185
|
+
count: sql(_templateObject4 || (_templateObject4 = _taggedTemplateLiteral(["COUNT(DISTINCT ", ")"])), table.id)
|
|
183
186
|
}).from(table).where(conditions.length > 0 ? and.apply(void 0, _toConsumableArray(conditions)) : undefined);
|
|
184
187
|
|
|
185
188
|
// 添加关联
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { TypedNextResponse } from "next-rest-framework";
|
|
2
|
-
import { BaseTable } from "../types";
|
|
3
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { BaseTable, AnyDatabase } from "../types";
|
|
4
3
|
import { NextRequest } from "next/server";
|
|
5
4
|
export interface DeleteOperationOptions<TTable extends BaseTable> {
|
|
6
5
|
table: TTable;
|
|
@@ -10,7 +9,7 @@ export interface DeleteOperationOptions<TTable extends BaseTable> {
|
|
|
10
9
|
byCreator?: boolean;
|
|
11
10
|
}
|
|
12
11
|
export declare const createDeleteOperation: ({ db, getSession, }: {
|
|
13
|
-
db:
|
|
12
|
+
db: AnyDatabase;
|
|
14
13
|
getSession: (req: NextRequest) => Promise<{
|
|
15
14
|
userId?: string;
|
|
16
15
|
} | undefined>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
2
|
import { NextRequest } from "next/server";
|
|
3
3
|
import { TypedNextRequest, TypedNextResponse } from "next-rest-framework";
|
|
4
|
-
import {
|
|
5
|
-
import { BaseTable, GetListRelations } from "../types";
|
|
4
|
+
import { BaseTable, GetListRelations, AnyDatabase } from "../types";
|
|
6
5
|
export interface GetListOperationOptions<T extends ZodSchema, Q extends ZodSchema, TTable extends BaseTable> {
|
|
7
6
|
bodySchema: T;
|
|
8
7
|
jsonArrayFields?: string[];
|
|
@@ -22,7 +21,7 @@ export interface GetListOperationOptions<T extends ZodSchema, Q extends ZodSchem
|
|
|
22
21
|
onError?: (error: Error) => Promise<ReturnType<(typeof TypedNextResponse)["json"]> | undefined>;
|
|
23
22
|
}
|
|
24
23
|
export declare const createGetListOperation: ({ db, getSession, }: {
|
|
25
|
-
db:
|
|
24
|
+
db: AnyDatabase;
|
|
26
25
|
getSession: (req: NextRequest) => Promise<{
|
|
27
26
|
userId?: string;
|
|
28
27
|
} | undefined>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
2
|
import { NextRequest } from "next/server";
|
|
3
3
|
import { TypedNextRequest, TypedNextResponse } from "next-rest-framework";
|
|
4
|
-
import {
|
|
5
|
-
import { BaseTable, GetListRelations } from "../types";
|
|
4
|
+
import { BaseTable, GetListRelations, AnyDatabase } from "../types";
|
|
6
5
|
export interface GetOperationOptions<T extends ZodSchema, Q extends ZodSchema, TTable extends BaseTable> {
|
|
7
6
|
bodySchema: T;
|
|
8
7
|
jsonArrayFields?: string[];
|
|
@@ -16,7 +15,7 @@ export interface GetOperationOptions<T extends ZodSchema, Q extends ZodSchema, T
|
|
|
16
15
|
byCreator?: boolean;
|
|
17
16
|
}
|
|
18
17
|
export declare const createGetOperation: ({ getSession, db, }: {
|
|
19
|
-
db:
|
|
18
|
+
db: AnyDatabase;
|
|
20
19
|
getSession: (req: NextRequest) => Promise<{
|
|
21
20
|
userId?: string;
|
|
22
21
|
} | undefined>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
declare function getTableName(table:
|
|
1
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
2
|
+
declare function getTableName(table: SQLiteTable): string;
|
|
3
3
|
export default getTableName;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { AnyDatabase } from "../types";
|
|
3
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
3
4
|
import { NextRequest } from "next/server";
|
|
4
5
|
import { TypedNextRequest, TypedNextResponse } from "next-rest-framework";
|
|
5
|
-
|
|
6
|
-
export interface PostOperationOptions<IB extends ZodSchema, OB extends ZodSchema, TTable extends PgTable> {
|
|
6
|
+
export interface PostOperationOptions<IB extends ZodSchema, OB extends ZodSchema, TTable extends SQLiteTable> {
|
|
7
7
|
bodySchema: IB;
|
|
8
8
|
outputBodySchema?: IB;
|
|
9
9
|
setBody?: (req: TypedNextRequest<"POST", "application/json", z.infer<IB>>) => Promise<Partial<z.infer<IB>>>;
|
|
@@ -13,8 +13,8 @@ export interface PostOperationOptions<IB extends ZodSchema, OB extends ZodSchema
|
|
|
13
13
|
table: TTable;
|
|
14
14
|
}
|
|
15
15
|
export declare const createPostOperation: ({ getSession, db, }: {
|
|
16
|
-
db:
|
|
16
|
+
db: AnyDatabase;
|
|
17
17
|
getSession: (req: NextRequest) => Promise<{
|
|
18
18
|
userId?: string;
|
|
19
19
|
} | undefined>;
|
|
20
|
-
}) => <IB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, OB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, TTable extends
|
|
20
|
+
}) => <IB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, OB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, TTable extends SQLiteTable<import("drizzle-orm/sqlite-core").TableConfig>>({ bodySchema, outputBodySchema, setBody, summary, table, onSuccess, onError, }: PostOperationOptions<IB, OB, TTable>) => import("next-rest-framework").RouteOperationDefinition<"POST">;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z, ZodSchema } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
3
3
|
import { TypedNextRequest, TypedNextResponse } from "next-rest-framework";
|
|
4
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
4
|
import { NextRequest } from "next/server";
|
|
6
|
-
import { BaseTable } from "../types";
|
|
7
|
-
export interface PutOperationOptions<IB extends ZodSchema, OB extends ZodSchema, TTable extends
|
|
5
|
+
import { BaseTable, AnyDatabase } from "../types";
|
|
6
|
+
export interface PutOperationOptions<IB extends ZodSchema, OB extends ZodSchema, TTable extends SQLiteTable> {
|
|
8
7
|
bodySchema: IB;
|
|
9
8
|
outputBodySchema?: OB;
|
|
10
9
|
table: TTable;
|
|
@@ -18,5 +17,5 @@ export declare const createPutOperation: ({ getSession, db, }: {
|
|
|
18
17
|
getSession: (req: NextRequest) => Promise<{
|
|
19
18
|
userId?: string;
|
|
20
19
|
} | undefined>;
|
|
21
|
-
db:
|
|
20
|
+
db: AnyDatabase;
|
|
22
21
|
}) => <IB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, OB extends z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, TTable extends BaseTable>({ bodySchema, outputBodySchema, table, summary, setBody, onSuccess, byCreator, onError, }: PutOperationOptions<IB, OB, TTable>) => import("next-rest-framework").RouteOperationDefinition<"PUT">;
|