@ai0x0/utils 0.1.1 → 0.1.3

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 CHANGED
@@ -68,7 +68,7 @@ export const getListOperation = createGetListOperation({ db, getSession });
68
68
 
69
69
  | Export | Purpose | Required options | Notable optional options |
70
70
  | ------------------- | -------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
71
- | `createTableSchema` | pg `Table` + 5 zod schemas | `name`, `columns` | `refineSchema`, `extraConfig` |
71
+ | `createTableSchema` | pg `Table` + 5 zod schemas | `name`, `columns` | `serverColumns`, `refineSchema`, `extraConfig` |
72
72
  | `getOperation` | GET one by filters | `schemas.query`, `schemas.response` | `table`, `setParams`, `relations`, `jsonArrayFields`, `access.byCreator`, `handler`, `catch`, `openApiOperation` |
73
73
  | `getListOperation` | GET paginated list | `schemas.query`, `schemas.response` | `table`, `setParams`, `relations`, `jsonArrayFields`, `handler`, `catch`, `openApiOperation` |
74
74
  | `postOperation` | POST create | `schemas.body` | `table`, `contentType`, `parseBody`, `schemas.response`, `setBody`, `handler`, `catch`, `openApiOperation` |
@@ -308,7 +308,8 @@ export const { GET } = route({
308
308
 
309
309
  ## Built-in Conventions
310
310
 
311
- - **Base fields** auto-added by `createTableSchema`: `id` (uuid, pk, default random) / `creatorId` / `editorId` / `accessedAt` / `createdAt` / `updatedAt`.
311
+ - **Base fields** auto-added by `createTableSchema`: `id` (uuid, pk, default random) / `creatorId` / `editorId` / `accessedAt` / `createdAt` / `updatedAt`. They never appear in the insert/update schemas — the server writes them.
312
+ - **Server-owned business columns** (`serverColumns: { ownerId: uuid("owner_id").notNull() }`): a second column bucket, alongside `columns`. Everything in it goes on the table and into `selectSchema` (you can read it), but **not** into `insertSchema` / `updateSchema` — exactly how the base fields already behave, just opened up to columns you declare yourself. Use it for anything carrying ownership (`ownerId`, `tenantId`): letting a client send that column is letting it choose whose row this is. Pair it with `access.scope.column` — that decides _which rows you see_, this decides _that you cannot write the column_. Which bucket a column sits in is the whole rule, so there is nothing to remember and nothing to forget; `insertSchema.omit(...)` per table would work too, but missing one table is not a compile error, it is a silent privilege escalation.
312
313
  - **Row-level scope** (`access`, on by default for every verb): the request may only see / touch rows whose scope column matches. Three knobs:
313
314
  - `byCreator?: boolean` — the old spelling, equivalent to `scope: { column: "creatorId", value: (s) => s.userId }`. Default `true`.
314
315
  - `scope?: { column?, value }` — pick the column (default `creatorId`) and derive the value from the session. Return an array for `IN (...)`. Use this when a row can belong to something other than one user (a team, a workspace).
@@ -87,9 +87,16 @@ export function createPostOperationFactory(_ref) {
87
87
  case 13:
88
88
  extraBody = _context.t0;
89
89
  // 新建的行归属这次请求的作用域。默认情形下作用域就是 creatorId = 当前用户,
90
- // 所以这与从前的行为逐字等价;换了归属列(比如 ownerId)时它自动跟着换。
90
+ // 换了归属列(比如 ownerId)时它自动跟着换。
91
91
  // 想再记一个「谁建的」用 setBody 补 —— 那是调用方的语义,库不猜。
92
- params = _objectSpread(_objectSpread(_objectSpread({}, scopeStamp(scope)), body), extraBody);
92
+ //
93
+ // **归属戳必须最后展开**,这是一条安全不变量,别为了「让调用方能覆盖」而调顺序:
94
+ // `.input({ body })` 只校验、**不替换**请求体(next-rest-framework 交给 handler 的
95
+ // clone 会重新解析原始 JSON),而 createPostAction 直接 insert,中间没有第二道
96
+ // schema 过滤。所以只要 body 排在戳后面,客户端在 JSON 里塞一个 `ownerId` /
97
+ // `creatorId` 就能把行写到别人名下 —— 调用方把该列从 schema 里 omit 掉也拦不住。
98
+ // 见 __tests__/route-operation-scope-stamp。
99
+ params = _objectSpread(_objectSpread(_objectSpread({}, body), extraBody), scopeStamp(scope));
93
100
  if (!(table && db)) {
94
101
  _context.next = 21;
95
102
  break;
@@ -183,7 +190,7 @@ export function createPutOperationFactory(_ref5) {
183
190
  status: 200
184
191
  }]).handler( /*#__PURE__*/function () {
185
192
  var _ref8 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(req) {
186
- var _yield$loadSession, loadSession, _yield$resolveAccess2, scope, body, extraBody, params, raw, data;
193
+ var _yield$loadSession, loadSession, _yield$resolveAccess2, scope, body, extraBody, editorId, params, raw, data;
187
194
  return _regeneratorRuntime().wrap(function _callee2$(_context2) {
188
195
  while (1) switch (_context2.prev = _context2.next) {
189
196
  case 0:
@@ -213,42 +220,38 @@ export function createPutOperationFactory(_ref5) {
213
220
  _context2.t0 = {};
214
221
  case 14:
215
222
  extraBody = _context2.t0;
216
- _context2.t1 = _objectSpread;
217
- _context2.t2 = _objectSpread;
218
- _context2.next = 19;
223
+ _context2.next = 17;
219
224
  return loadSession();
220
- case 19:
221
- _context2.t4 = _yield$loadSession = _context2.sent;
222
- _context2.t3 = _context2.t4 === null;
223
- if (_context2.t3) {
224
- _context2.next = 23;
225
+ case 17:
226
+ _context2.t2 = _yield$loadSession = _context2.sent;
227
+ _context2.t1 = _context2.t2 === null;
228
+ if (_context2.t1) {
229
+ _context2.next = 21;
225
230
  break;
226
231
  }
227
- _context2.t3 = _yield$loadSession === void 0;
228
- case 23:
229
- if (!_context2.t3) {
230
- _context2.next = 27;
232
+ _context2.t1 = _yield$loadSession === void 0;
233
+ case 21:
234
+ if (!_context2.t1) {
235
+ _context2.next = 25;
231
236
  break;
232
237
  }
233
- _context2.t5 = void 0;
234
- _context2.next = 28;
238
+ _context2.t3 = void 0;
239
+ _context2.next = 26;
235
240
  break;
236
- case 27:
237
- _context2.t5 = _yield$loadSession.userId;
238
- case 28:
239
- _context2.t6 = _context2.t5;
240
- _context2.t7 = {
241
- editorId: _context2.t6
242
- };
243
- _context2.t8 = body;
244
- _context2.t9 = (0, _context2.t2)(_context2.t7, _context2.t8);
245
- _context2.t10 = extraBody;
246
- params = (0, _context2.t1)(_context2.t9, _context2.t10);
241
+ case 25:
242
+ _context2.t3 = _yield$loadSession.userId;
243
+ case 26:
244
+ editorId = _context2.t3;
245
+ // editorId 同样**最后**展开:它是审计字段,让请求体覆盖等于让人随便写「是谁改的」。
246
+ // 理由与 POST 那边的归属戳一样,见那里的说明。
247
+ params = _objectSpread(_objectSpread(_objectSpread({}, body), extraBody), {}, {
248
+ editorId: editorId
249
+ });
247
250
  if (!(table && db)) {
248
- _context2.next = 40;
251
+ _context2.next = 34;
249
252
  break;
250
253
  }
251
- _context2.next = 37;
254
+ _context2.next = 31;
252
255
  return createAction({
253
256
  bodySchema: schemas.body,
254
257
  db: db,
@@ -256,44 +259,44 @@ export function createPutOperationFactory(_ref5) {
256
259
  })(params, {
257
260
  scope: scope
258
261
  });
259
- case 37:
260
- _context2.t11 = _context2.sent;
261
- _context2.next = 41;
262
+ case 31:
263
+ _context2.t4 = _context2.sent;
264
+ _context2.next = 35;
262
265
  break;
263
- case 40:
264
- _context2.t11 = params;
265
- case 41:
266
- raw = _context2.t11;
266
+ case 34:
267
+ _context2.t4 = params;
268
+ case 35:
269
+ raw = _context2.t4;
267
270
  if (!handler) {
268
- _context2.next = 48;
271
+ _context2.next = 42;
269
272
  break;
270
273
  }
271
- _context2.next = 45;
274
+ _context2.next = 39;
272
275
  return handler({
273
276
  data: raw,
274
277
  params: params,
275
278
  req: req
276
279
  });
277
- case 45:
278
- _context2.t12 = _context2.sent;
279
- _context2.next = 49;
280
+ case 39:
281
+ _context2.t5 = _context2.sent;
282
+ _context2.next = 43;
280
283
  break;
281
- case 48:
282
- _context2.t12 = raw;
283
- case 49:
284
- data = _context2.t12;
284
+ case 42:
285
+ _context2.t5 = raw;
286
+ case 43:
287
+ data = _context2.t5;
285
288
  return _context2.abrupt("return", TypedNextResponse.json(data, {
286
289
  status: 200
287
290
  }));
288
- case 53:
289
- _context2.prev = 53;
290
- _context2.t13 = _context2["catch"](0);
291
- return _context2.abrupt("return", handleOperationError(_context2.t13, catchHandler));
292
- case 56:
291
+ case 47:
292
+ _context2.prev = 47;
293
+ _context2.t6 = _context2["catch"](0);
294
+ return _context2.abrupt("return", handleOperationError(_context2.t6, catchHandler));
295
+ case 50:
293
296
  case "end":
294
297
  return _context2.stop();
295
298
  }
296
- }, _callee2, null, [[0, 53]]);
299
+ }, _callee2, null, [[0, 47]]);
297
300
  }));
298
301
  return function (_x2) {
299
302
  return _ref8.apply(this, arguments);