@ai0x0/utils 0.2.0 → 0.4.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.
@@ -48,7 +48,7 @@ export function createPostOperationFactory(_ref) {
48
48
  contentType: contentType
49
49
  }).outputs([{
50
50
  body: schemas.response || z.object({
51
- id: z.string()
51
+ id: z.string().describe("Id of the row created.")
52
52
  }),
53
53
  contentType: "application/json",
54
54
  status: 200
@@ -316,7 +316,7 @@ export function createPutOperationFactory(_ref5) {
316
316
  // ==============================================================================
317
317
 
318
318
  var defaultDeleteBodySchema = z.object({
319
- id: z.string()
319
+ id: z.string().describe("Id of the row to delete.")
320
320
  });
321
321
  export function createDeleteOperationFactory(_ref9) {
322
322
  var createAction = _ref9.createAction,
@@ -113,13 +113,33 @@ export declare const listBodySchema: <T extends z.ZodType<unknown, unknown, z.co
113
113
  total: z.ZodNumber;
114
114
  data: z.ZodArray<T>;
115
115
  }, z.core.$strip>;
116
+ /**
117
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
118
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
119
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
120
+ */
121
+ export declare const describeFields: <T extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: T, notes: Record<string, string>) => T;
116
122
  /**
117
123
  * 基于 drizzle + drizzle-zod 快速生成带基础字段(id / 创建时间等)的
118
124
  * 表定义以及 select / insert / update / query / list zod schema。
119
125
  */
120
- export declare const createTableSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>>, TServerColumns extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>> = {}>({ name, columns, serverColumns, extraConfig, }: {
126
+ export declare const createTableSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>>, TServerColumns extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>> = {}>({ name, columns, serverColumns, extraConfig, describe, }: {
121
127
  /** 客户端能写的业务列。insert / update schema 只从这里推。 */
122
128
  columns: TColumnsMap;
129
+ /**
130
+ * 每一列的说明,键是列名。**同时挂到 insert / update / select 三份 schema 上** —— 同一列在
131
+ * 请求体里和响应里是同一个东西,说明没有理由写两遍。
132
+ *
133
+ * 为什么需要这个参数:drizzle 列没有「描述」这个概念,而 `.describe()` 只能挂在 zod 上。不给
134
+ * 这条路,业务侧就只能在每张表后面自己把三份 schema 各 `.extend()` 一遍 —— 那是三份副本,
135
+ * 而副本会漂。
136
+ *
137
+ * 键有类型约束(列名的联合),所以列改名了、拼错了都是编译错误,不是静默失效。
138
+ *
139
+ * 说明会出现在 OpenAPI spec 里,再流进按 spec 生成的 client 与文档 —— 所以它是写给调用方
140
+ * 看的:这一列是什么、合法值从哪来、不传会怎样。
141
+ */
142
+ describe?: Partial<Record<Extract<keyof TColumnsMap, string> | Extract<keyof TServerColumns, string>, string>> | undefined;
123
143
  name: TTableName;
124
144
  /**
125
145
  * 由服务端盖、**绝不接受客户端传**的业务列:承载归属的 `ownerId`(配合
@@ -4,6 +4,12 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
5
5
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
6
6
  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); }
7
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
8
+ 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."); }
9
+ 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); }
10
+ 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; }
11
+ 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; } }
12
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
13
  import { z } from "zod";
8
14
  import { pgTable, timestamp, uuid } from "drizzle-orm/pg-core";
9
15
  import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-zod";
@@ -72,19 +78,21 @@ export var BASIC_UPDATE_OMIT = {
72
78
  // =============================================================================
73
79
  // 列表查询字段
74
80
  // =============================================================================
81
+ // 说明写成 `.describe()` 而不是行末注释:注释只有读源码的人看得到,而这七个字段会出现在每一个
82
+ // list 端点的 OpenAPI spec 里,再从那里流进生成的 client 与 CLI 文档。写成注释的代价是下游只能
83
+ // 自己再手写一份 —— 那份必然会漂,而且漂了没有任何东西会报错。
84
+ //
85
+ // 类型是 string 而不是 number:query 参数在线上只有字符串,服务端收下之后才转数字。默认值也
86
+ // 因此是 "1" / "10"。
75
87
  export var queryListSchema = function queryListSchema(schema) {
76
88
  return z.object({
77
- current: z.string().optional().default("1"),
78
- // 默认页码为 1
79
- pageSize: z.string().optional().default("10"),
80
- // 默认每页条数为 10
81
- createdAtFrom: z.string().optional(),
82
- // 筛选开始日期
83
- createdAtTo: z.string().optional(),
84
- // 筛选结束日期
85
- orderBy: z.string().optional(),
86
- creatorId: z.string().optional(),
87
- orderDir: z.enum(["asc", "desc"]).optional()
89
+ current: z.string().optional().default("1").describe("Page number, starting at 1."),
90
+ pageSize: z.string().optional().default("10").describe("Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."),
91
+ createdAtFrom: z.string().optional().describe("Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."),
92
+ createdAtTo: z.string().optional().describe("Keep only rows created at or before this instant."),
93
+ orderBy: z.string().optional().describe("Column to sort by — a column name of this resource (createdAt / updatedAt and the like)."),
94
+ creatorId: z.string().optional().describe("Keep only rows created by this user. Useful in shared spaces to filter down to one member."),
95
+ orderDir: z.enum(["asc", "desc"]).optional().describe("Sort ascending or descending.")
88
96
  }).merge(schema);
89
97
  };
90
98
 
@@ -98,6 +106,74 @@ export var listBodySchema = function listBodySchema(schema) {
98
106
  });
99
107
  };
100
108
 
109
+ // =============================================================================
110
+ // 给基础字段挂说明
111
+ // =============================================================================
112
+ // basicFields 是 drizzle 列,而 drizzle 列没有「描述」这个概念 —— 所以说明只能挂在
113
+ // drizzle-zod 出来的 schema 上。值得做是因为这六个字段出现在**每一张表**的 select schema 里:
114
+ // 一处写完,所有 list / get 端点的返回字段表都有了(在 do-tv 上是 173 行)。
115
+ //
116
+ // 一律只挂元数据,**不换类型**:`.describe()` / `.meta()` 返回的是带元数据的克隆,校验行为一模
117
+ // 一样。selectSchema 是有人拿去 safeParse 的(本包自己的测试就在用 Date 校验它),换类型等于改
118
+ // 这个包的契约。
119
+ //
120
+ // 时间戳那三列额外用 `.meta()` 补上 JSON Schema 表示,因为不补就是错的:drizzle-zod 给的是
121
+ // `z.date()`,而 `Date` 在 JSON Schema 里表达不出来,于是这三个字段在 spec 里是**空对象** ——
122
+ // 没有 `type`,照 spec 生成的 client / CLI 只能得到 `unknown`。而线上根本不可能是 Date:响应一经
123
+ // JSON 序列化就是 ISO 8601 字符串。
124
+ //
125
+ // `.meta({ type, format })` 恰好只改「怎么描述」不改「怎么校验」:spec 里成为
126
+ // `{"type":"string","format":"date-time"}`,而 `safeParse(new Date())` 照样通过。
127
+ // 不用 `z.iso.datetime()` 换掉它,是因为那既改了校验(不再收 Date),又会往 JSON Schema 里塞一条
128
+ // 380 字节的正则 —— 乘上每张表几十 KB,而 `format` 已经把「按日期时间解析」说清楚了。
129
+ var BASIC_FIELD_NOTES = {
130
+ creatorId: "User id of the creator. Stamped by the server; clients cannot send it.",
131
+ editorId: "User id of whoever changed this row last. Stamped by the server; clients cannot send it.",
132
+ id: "Primary key of this row (uuid)."
133
+ };
134
+ var BASIC_TIMESTAMP_NOTES = {
135
+ accessedAt: "When this row was last accessed.",
136
+ createdAt: "When this row was created.",
137
+ updatedAt: "When this row was last modified."
138
+ };
139
+
140
+ /**
141
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
142
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
143
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
144
+ */
145
+ export var describeFields = function describeFields(schema, notes) {
146
+ var overlay = {};
147
+ for (var _i = 0, _Object$entries = Object.entries(notes); _i < _Object$entries.length; _i++) {
148
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
149
+ field = _Object$entries$_i[0],
150
+ note = _Object$entries$_i[1];
151
+ var existing = schema.shape[field];
152
+ if (existing) {
153
+ overlay[field] = existing.describe(note);
154
+ }
155
+ }
156
+ return Object.keys(overlay).length ? schema.extend(overlay) : schema;
157
+ };
158
+ var describeBasicFields = function describeBasicFields(schema) {
159
+ var withNotes = describeFields(schema, BASIC_FIELD_NOTES);
160
+ var overlay = {};
161
+ for (var _i2 = 0, _Object$entries2 = Object.entries(BASIC_TIMESTAMP_NOTES); _i2 < _Object$entries2.length; _i2++) {
162
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
163
+ field = _Object$entries2$_i[0],
164
+ note = _Object$entries2$_i[1];
165
+ var existing = withNotes.shape[field];
166
+ if (existing) {
167
+ overlay[field] = existing.meta({
168
+ description: note,
169
+ format: "date-time",
170
+ type: "string"
171
+ });
172
+ }
173
+ }
174
+ return Object.keys(overlay).length ? withNotes.extend(overlay) : withNotes;
175
+ };
176
+
101
177
  // =============================================================================
102
178
  // 建表 —— 表 + 5 份 zod schema
103
179
  // =============================================================================
@@ -109,19 +185,23 @@ export var createTableSchema = function createTableSchema(_ref) {
109
185
  var name = _ref.name,
110
186
  columns = _ref.columns,
111
187
  serverColumns = _ref.serverColumns,
112
- extraConfig = _ref.extraConfig;
188
+ extraConfig = _ref.extraConfig,
189
+ describe = _ref.describe;
113
190
  var mergedColumns = _objectSpread(_objectSpread(_objectSpread({}, basicFields), serverColumns), columns);
114
191
  var table = pgTable(name, mergedColumns, extraConfig);
115
- var selectSchema = createSelectSchema(table);
192
+
193
+ // 同一份说明喂给三份 schema:含哪些列各不相同,describeFields 会跳过不存在的。
194
+ var notes = describe !== null && describe !== void 0 ? describe : {};
195
+ var selectSchema = describeFields(describeBasicFields(createSelectSchema(table)), notes);
116
196
  // 只从 columns 推 —— basicFields 与 serverColumns 都由服务端写,不该出现在请求体里。
117
- var insertSchema = createInsertSchema(pgTable(name, columns));
118
- var updateSchema = createUpdateSchema(pgTable(name, _objectSpread({
197
+ var insertSchema = describeFields(createInsertSchema(pgTable(name, columns)), notes);
198
+ var updateSchema = describeFields(createUpdateSchema(pgTable(name, _objectSpread({
119
199
  id: uuid("id")
120
200
  }, columns))).extend({
121
- id: z.string()
122
- });
201
+ id: z.string().describe("Id of the row to update.")
202
+ }), notes);
123
203
  var querySchema = z.object({
124
- id: z.string()
204
+ id: z.string().describe("Id of the row to fetch.")
125
205
  });
126
206
  var queryListWithSchema = queryListSchema(z.object({}).catchall(z.unknown()));
127
207
  return {
@@ -113,6 +113,12 @@ export declare const listBodySchema: <T extends z.ZodType<unknown, unknown, z.co
113
113
  total: z.ZodNumber;
114
114
  data: z.ZodArray<T>;
115
115
  }, z.core.$strip>;
116
+ /**
117
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
118
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
119
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
120
+ */
121
+ export declare const describeFields: <T extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: T, notes: Record<string, string>) => T;
116
122
  /**
117
123
  * 基于 drizzle + drizzle-zod 快速生成带基础字段(id / 创建时间等)的
118
124
  * 表定义以及 select / insert / update / query / list zod schema。
@@ -121,6 +127,11 @@ export declare const createTableSchema: <TTableName extends string, TColumnsMap
121
127
  name: TTableName;
122
128
  /** 客户端能写的业务列。insert / update schema 只从这里推。 */
123
129
  columns: TColumnsMap;
130
+ /**
131
+ * 每一列的说明,键是列名。同时挂到 insert / update / select 三份 schema 上。语义与 pg 版
132
+ * 一致,见那边的说明。
133
+ */
134
+ describe?: Partial<Record<string, string>>;
124
135
  /** 由服务端盖、绝不接受客户端传的业务列(归属、租户 id 这类)。语义与 pg 版一致,见那边的说明。 */
125
136
  serverColumns?: Record<string, SQLiteColumnBuilderBase>;
126
137
  extraConfig?: (_self: BuildExtraConfigColumns<TTableName, typeof basicFields & TColumnsMap, "sqlite">) => SQLiteTableExtraConfigValue[];
@@ -4,6 +4,12 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
5
5
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
6
6
  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); }
7
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
8
+ 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."); }
9
+ 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); }
10
+ 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; }
11
+ 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; } }
12
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
13
  import { z } from "zod";
8
14
  import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
9
15
  import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-zod";
@@ -78,19 +84,21 @@ export var BASIC_UPDATE_OMIT = {
78
84
  // =============================================================================
79
85
  // 列表查询字段
80
86
  // =============================================================================
87
+ // 说明写成 `.describe()` 而不是行末注释:注释只有读源码的人看得到,而这七个字段会出现在每一个
88
+ // list 端点的 OpenAPI spec 里,再从那里流进生成的 client 与 CLI 文档。写成注释的代价是下游只能
89
+ // 自己再手写一份 —— 那份必然会漂,而且漂了没有任何东西会报错。
90
+ //
91
+ // 类型是 string 而不是 number:query 参数在线上只有字符串,服务端收下之后才转数字。默认值也
92
+ // 因此是 "1" / "10"。
81
93
  export var queryListSchema = function queryListSchema(schema) {
82
94
  return z.object({
83
- current: z.string().optional().default("1"),
84
- // 默认页码为 1
85
- pageSize: z.string().optional().default("10"),
86
- // 默认每页条数为 10
87
- createdAtFrom: z.string().optional(),
88
- // 筛选开始日期
89
- createdAtTo: z.string().optional(),
90
- // 筛选结束日期
91
- orderBy: z.string().optional(),
92
- creatorId: z.string().optional(),
93
- orderDir: z.enum(["asc", "desc"]).optional()
95
+ current: z.string().optional().default("1").describe("Page number, starting at 1."),
96
+ pageSize: z.string().optional().default("10").describe("Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."),
97
+ createdAtFrom: z.string().optional().describe("Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."),
98
+ createdAtTo: z.string().optional().describe("Keep only rows created at or before this instant."),
99
+ orderBy: z.string().optional().describe("Column to sort by — a column name of this resource (createdAt / updatedAt and the like)."),
100
+ creatorId: z.string().optional().describe("Keep only rows created by this user. Useful in shared spaces to filter down to one member."),
101
+ orderDir: z.enum(["asc", "desc"]).optional().describe("Sort ascending or descending.")
94
102
  }).merge(schema);
95
103
  };
96
104
 
@@ -104,6 +112,74 @@ export var listBodySchema = function listBodySchema(schema) {
104
112
  });
105
113
  };
106
114
 
115
+ // =============================================================================
116
+ // 给基础字段挂说明
117
+ // =============================================================================
118
+ // basicFields 是 drizzle 列,而 drizzle 列没有「描述」这个概念 —— 所以说明只能挂在
119
+ // drizzle-zod 出来的 schema 上。值得做是因为这六个字段出现在**每一张表**的 select schema 里:
120
+ // 一处写完,所有 list / get 端点的返回字段都有说明了。
121
+ //
122
+ // 一律只挂元数据,**不换类型**:`.describe()` / `.meta()` 返回的是带元数据的克隆,校验行为一模
123
+ // 一样。selectSchema 是有人拿去 safeParse 的(本包自己的测试就在用 Date 校验它),换类型等于改
124
+ // 这个包的契约。
125
+ //
126
+ // 时间戳那三列额外用 `.meta()` 补上 JSON Schema 表示,因为不补就是错的:drizzle-zod 给的是
127
+ // `z.date()`,而 `Date` 在 JSON Schema 里表达不出来,于是这三个字段在 spec 里是**空对象** ——
128
+ // 没有 `type`,照 spec 生成的 client / CLI 只能得到 `unknown`。而线上根本不可能是 Date:响应一经
129
+ // JSON 序列化就是 ISO 8601 字符串。
130
+ //
131
+ // `.meta({ type, format })` 恰好只改「怎么描述」不改「怎么校验」:spec 里成为
132
+ // `{"type":"string","format":"date-time"}`,而 `safeParse(new Date())` 照样通过。
133
+ // 不用 `z.iso.datetime()` 换掉它,是因为那既改了校验(不再收 Date),又会往 JSON Schema 里塞一条
134
+ // 380 字节的正则 —— 乘上每张表几十 KB,而 `format` 已经把「按日期时间解析」说清楚了。
135
+ var BASIC_FIELD_NOTES = {
136
+ creatorId: "User id of the creator. Stamped by the server; clients cannot send it.",
137
+ editorId: "User id of whoever changed this row last. Stamped by the server; clients cannot send it.",
138
+ id: "Primary key of this row (uuid)."
139
+ };
140
+ var BASIC_TIMESTAMP_NOTES = {
141
+ accessedAt: "When this row was last accessed.",
142
+ createdAt: "When this row was created.",
143
+ updatedAt: "When this row was last modified."
144
+ };
145
+
146
+ /**
147
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
148
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
149
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
150
+ */
151
+ export var describeFields = function describeFields(schema, notes) {
152
+ var overlay = {};
153
+ for (var _i = 0, _Object$entries = Object.entries(notes); _i < _Object$entries.length; _i++) {
154
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
155
+ field = _Object$entries$_i[0],
156
+ note = _Object$entries$_i[1];
157
+ var existing = schema.shape[field];
158
+ if (existing) {
159
+ overlay[field] = existing.describe(note);
160
+ }
161
+ }
162
+ return Object.keys(overlay).length ? schema.extend(overlay) : schema;
163
+ };
164
+ var describeBasicFields = function describeBasicFields(schema) {
165
+ var withNotes = describeFields(schema, BASIC_FIELD_NOTES);
166
+ var overlay = {};
167
+ for (var _i2 = 0, _Object$entries2 = Object.entries(BASIC_TIMESTAMP_NOTES); _i2 < _Object$entries2.length; _i2++) {
168
+ var _Object$entries2$_i = _slicedToArray(_Object$entries2[_i2], 2),
169
+ field = _Object$entries2$_i[0],
170
+ note = _Object$entries2$_i[1];
171
+ var existing = withNotes.shape[field];
172
+ if (existing) {
173
+ overlay[field] = existing.meta({
174
+ description: note,
175
+ format: "date-time",
176
+ type: "string"
177
+ });
178
+ }
179
+ }
180
+ return Object.keys(overlay).length ? withNotes.extend(overlay) : withNotes;
181
+ };
182
+
107
183
  // =============================================================================
108
184
  // 建表 —— 表 + 5 份 zod schema
109
185
  // =============================================================================
@@ -115,19 +191,23 @@ export var createTableSchema = function createTableSchema(_ref) {
115
191
  var name = _ref.name,
116
192
  columns = _ref.columns,
117
193
  serverColumns = _ref.serverColumns,
118
- extraConfig = _ref.extraConfig;
194
+ extraConfig = _ref.extraConfig,
195
+ describe = _ref.describe;
119
196
  var mergedColumns = _objectSpread(_objectSpread(_objectSpread({}, basicFields), serverColumns), columns);
120
197
  var table = sqliteTable(name, mergedColumns, extraConfig);
121
- var selectSchema = createSelectSchema(table);
198
+
199
+ // 同一份说明喂给三份 schema:含哪些列各不相同,describeFields 会跳过不存在的。
200
+ var notes = describe !== null && describe !== void 0 ? describe : {};
201
+ var selectSchema = describeFields(describeBasicFields(createSelectSchema(table)), notes);
122
202
  // 只从 columns 推 —— basicFields 与 serverColumns 都由服务端写,不该出现在请求体里。
123
- var insertSchema = createInsertSchema(sqliteTable(name, columns));
124
- var updateSchema = createUpdateSchema(sqliteTable(name, _objectSpread({
203
+ var insertSchema = describeFields(createInsertSchema(sqliteTable(name, columns)), notes);
204
+ var updateSchema = describeFields(createUpdateSchema(sqliteTable(name, _objectSpread({
125
205
  id: text("id")
126
206
  }, columns))).extend({
127
- id: z.string()
128
- });
207
+ id: z.string().describe("Id of the row to update.")
208
+ }), notes);
129
209
  var querySchema = z.object({
130
- id: z.string()
210
+ id: z.string().describe("Id of the row to fetch.")
131
211
  });
132
212
  var queryListWithSchema = queryListSchema(z.object({}).catchall(z.unknown()));
133
213
  return {
@@ -57,7 +57,7 @@ function createPostOperationFactory({
57
57
  contentType
58
58
  }).outputs([
59
59
  {
60
- body: schemas.response || import_zod.z.object({ id: import_zod.z.string() }),
60
+ body: schemas.response || import_zod.z.object({ id: import_zod.z.string().describe("Id of the row created.") }),
61
61
  contentType: "application/json",
62
62
  status: 200
63
63
  }
@@ -156,7 +156,7 @@ function createPutOperationFactory({
156
156
  });
157
157
  }
158
158
  var defaultDeleteBodySchema = import_zod.z.object({
159
- id: import_zod.z.string()
159
+ id: import_zod.z.string().describe("Id of the row to delete.")
160
160
  });
161
161
  function createDeleteOperationFactory({
162
162
  createAction,
@@ -113,13 +113,33 @@ export declare const listBodySchema: <T extends z.ZodType<unknown, unknown, z.co
113
113
  total: z.ZodNumber;
114
114
  data: z.ZodArray<T>;
115
115
  }, z.core.$strip>;
116
+ /**
117
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
118
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
119
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
120
+ */
121
+ export declare const describeFields: <T extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: T, notes: Record<string, string>) => T;
116
122
  /**
117
123
  * 基于 drizzle + drizzle-zod 快速生成带基础字段(id / 创建时间等)的
118
124
  * 表定义以及 select / insert / update / query / list zod schema。
119
125
  */
120
- export declare const createTableSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>>, TServerColumns extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>> = {}>({ name, columns, serverColumns, extraConfig, }: {
126
+ export declare const createTableSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>>, TServerColumns extends Record<string, PgColumnBuilderBase<import("drizzle-orm").ColumnBuilderBaseConfig<import("drizzle-orm").ColumnDataType, string>, object>> = {}>({ name, columns, serverColumns, extraConfig, describe, }: {
121
127
  /** 客户端能写的业务列。insert / update schema 只从这里推。 */
122
128
  columns: TColumnsMap;
129
+ /**
130
+ * 每一列的说明,键是列名。**同时挂到 insert / update / select 三份 schema 上** —— 同一列在
131
+ * 请求体里和响应里是同一个东西,说明没有理由写两遍。
132
+ *
133
+ * 为什么需要这个参数:drizzle 列没有「描述」这个概念,而 `.describe()` 只能挂在 zod 上。不给
134
+ * 这条路,业务侧就只能在每张表后面自己把三份 schema 各 `.extend()` 一遍 —— 那是三份副本,
135
+ * 而副本会漂。
136
+ *
137
+ * 键有类型约束(列名的联合),所以列改名了、拼错了都是编译错误,不是静默失效。
138
+ *
139
+ * 说明会出现在 OpenAPI spec 里,再流进按 spec 生成的 client 与文档 —— 所以它是写给调用方
140
+ * 看的:这一列是什么、合法值从哪来、不传会怎样。
141
+ */
142
+ describe?: Partial<Record<Extract<keyof TColumnsMap, string> | Extract<keyof TServerColumns, string>, string>> | undefined;
123
143
  name: TTableName;
124
144
  /**
125
145
  * 由服务端盖、**绝不接受客户端传**的业务列:承载归属的 `ownerId`(配合
@@ -23,6 +23,7 @@ __export(schemas_exports, {
23
23
  BASIC_UPDATE_OMIT: () => BASIC_UPDATE_OMIT,
24
24
  basicFields: () => basicFields,
25
25
  createTableSchema: () => createTableSchema,
26
+ describeFields: () => describeFields,
26
27
  listBodySchema: () => listBodySchema,
27
28
  queryListSchema: () => queryListSchema
28
29
  });
@@ -58,27 +59,67 @@ var BASIC_UPDATE_OMIT = {
58
59
  updatedAt: true
59
60
  };
60
61
  var queryListSchema = (schema) => import_zod.z.object({
61
- current: import_zod.z.string().optional().default("1"),
62
- // 默认页码为 1
63
- pageSize: import_zod.z.string().optional().default("10"),
64
- // 默认每页条数为 10
65
- createdAtFrom: import_zod.z.string().optional(),
66
- // 筛选开始日期
67
- createdAtTo: import_zod.z.string().optional(),
68
- // 筛选结束日期
69
- orderBy: import_zod.z.string().optional(),
70
- creatorId: import_zod.z.string().optional(),
71
- orderDir: import_zod.z.enum(["asc", "desc"]).optional()
62
+ current: import_zod.z.string().optional().default("1").describe("Page number, starting at 1."),
63
+ pageSize: import_zod.z.string().optional().default("10").describe(
64
+ "Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."
65
+ ),
66
+ createdAtFrom: import_zod.z.string().optional().describe(
67
+ "Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."
68
+ ),
69
+ createdAtTo: import_zod.z.string().optional().describe("Keep only rows created at or before this instant."),
70
+ orderBy: import_zod.z.string().optional().describe(
71
+ "Column to sort by — a column name of this resource (createdAt / updatedAt and the like)."
72
+ ),
73
+ creatorId: import_zod.z.string().optional().describe(
74
+ "Keep only rows created by this user. Useful in shared spaces to filter down to one member."
75
+ ),
76
+ orderDir: import_zod.z.enum(["asc", "desc"]).optional().describe("Sort ascending or descending.")
72
77
  }).merge(schema);
73
78
  var listBodySchema = (schema) => import_zod.z.object({
74
79
  total: import_zod.z.number(),
75
80
  data: import_zod.z.array(schema)
76
81
  });
82
+ var BASIC_FIELD_NOTES = {
83
+ creatorId: "User id of the creator. Stamped by the server; clients cannot send it.",
84
+ editorId: "User id of whoever changed this row last. Stamped by the server; clients cannot send it.",
85
+ id: "Primary key of this row (uuid)."
86
+ };
87
+ var BASIC_TIMESTAMP_NOTES = {
88
+ accessedAt: "When this row was last accessed.",
89
+ createdAt: "When this row was created.",
90
+ updatedAt: "When this row was last modified."
91
+ };
92
+ var describeFields = (schema, notes) => {
93
+ const overlay = {};
94
+ for (const [field, note] of Object.entries(notes)) {
95
+ const existing = schema.shape[field];
96
+ if (existing) {
97
+ overlay[field] = existing.describe(note);
98
+ }
99
+ }
100
+ return Object.keys(overlay).length ? schema.extend(overlay) : schema;
101
+ };
102
+ var describeBasicFields = (schema) => {
103
+ const withNotes = describeFields(schema, BASIC_FIELD_NOTES);
104
+ const overlay = {};
105
+ for (const [field, note] of Object.entries(BASIC_TIMESTAMP_NOTES)) {
106
+ const existing = withNotes.shape[field];
107
+ if (existing) {
108
+ overlay[field] = existing.meta({
109
+ description: note,
110
+ format: "date-time",
111
+ type: "string"
112
+ });
113
+ }
114
+ }
115
+ return Object.keys(overlay).length ? withNotes.extend(overlay) : withNotes;
116
+ };
77
117
  var createTableSchema = ({
78
118
  name,
79
119
  columns,
80
120
  serverColumns,
81
- extraConfig
121
+ extraConfig,
122
+ describe
82
123
  }) => {
83
124
  const mergedColumns = {
84
125
  ...basicFields,
@@ -86,12 +127,24 @@ var createTableSchema = ({
86
127
  ...columns
87
128
  };
88
129
  const table = (0, import_pg_core.pgTable)(name, mergedColumns, extraConfig);
89
- const selectSchema = (0, import_drizzle_zod.createSelectSchema)(table);
90
- const insertSchema = (0, import_drizzle_zod.createInsertSchema)((0, import_pg_core.pgTable)(name, columns));
91
- const updateSchema = (0, import_drizzle_zod.createUpdateSchema)(
92
- (0, import_pg_core.pgTable)(name, { id: (0, import_pg_core.uuid)("id"), ...columns })
93
- ).extend({ id: import_zod.z.string() });
94
- const querySchema = import_zod.z.object({ id: import_zod.z.string() });
130
+ const notes = describe ?? {};
131
+ const selectSchema = describeFields(
132
+ describeBasicFields((0, import_drizzle_zod.createSelectSchema)(table)),
133
+ notes
134
+ );
135
+ const insertSchema = describeFields(
136
+ (0, import_drizzle_zod.createInsertSchema)((0, import_pg_core.pgTable)(name, columns)),
137
+ notes
138
+ );
139
+ const updateSchema = describeFields(
140
+ (0, import_drizzle_zod.createUpdateSchema)((0, import_pg_core.pgTable)(name, { id: (0, import_pg_core.uuid)("id"), ...columns })).extend({
141
+ id: import_zod.z.string().describe("Id of the row to update.")
142
+ }),
143
+ notes
144
+ );
145
+ const querySchema = import_zod.z.object({
146
+ id: import_zod.z.string().describe("Id of the row to fetch.")
147
+ });
95
148
  const queryListWithSchema = queryListSchema(
96
149
  import_zod.z.object({}).catchall(import_zod.z.unknown())
97
150
  );
@@ -111,6 +164,7 @@ var createTableSchema = ({
111
164
  BASIC_UPDATE_OMIT,
112
165
  basicFields,
113
166
  createTableSchema,
167
+ describeFields,
114
168
  listBodySchema,
115
169
  queryListSchema
116
170
  });
@@ -113,6 +113,12 @@ export declare const listBodySchema: <T extends z.ZodType<unknown, unknown, z.co
113
113
  total: z.ZodNumber;
114
114
  data: z.ZodArray<T>;
115
115
  }, z.core.$strip>;
116
+ /**
117
+ * 给 schema 上已有的字段挂说明。字段不在这个 schema 里就跳过 —— 同一张表的
118
+ * insert / update / select 三份含的列不一样(basicFields 只在 select 里),一份说明表要能同时
119
+ * 喂给三者,就不能对「字段一定存在」有要求。**不给不存在的字段凭空造一个。**
120
+ */
121
+ export declare const describeFields: <T extends z.ZodObject<z.core.$ZodLooseShape, z.core.$strip>>(schema: T, notes: Record<string, string>) => T;
116
122
  /**
117
123
  * 基于 drizzle + drizzle-zod 快速生成带基础字段(id / 创建时间等)的
118
124
  * 表定义以及 select / insert / update / query / list zod schema。
@@ -121,6 +127,11 @@ export declare const createTableSchema: <TTableName extends string, TColumnsMap
121
127
  name: TTableName;
122
128
  /** 客户端能写的业务列。insert / update schema 只从这里推。 */
123
129
  columns: TColumnsMap;
130
+ /**
131
+ * 每一列的说明,键是列名。同时挂到 insert / update / select 三份 schema 上。语义与 pg 版
132
+ * 一致,见那边的说明。
133
+ */
134
+ describe?: Partial<Record<string, string>>;
124
135
  /** 由服务端盖、绝不接受客户端传的业务列(归属、租户 id 这类)。语义与 pg 版一致,见那边的说明。 */
125
136
  serverColumns?: Record<string, SQLiteColumnBuilderBase>;
126
137
  extraConfig?: (_self: BuildExtraConfigColumns<TTableName, typeof basicFields & TColumnsMap, "sqlite">) => SQLiteTableExtraConfigValue[];
@@ -23,6 +23,7 @@ __export(schemas_exports, {
23
23
  BASIC_UPDATE_OMIT: () => BASIC_UPDATE_OMIT,
24
24
  basicFields: () => basicFields,
25
25
  createTableSchema: () => createTableSchema,
26
+ describeFields: () => describeFields,
26
27
  listBodySchema: () => listBodySchema,
27
28
  queryListSchema: () => queryListSchema
28
29
  });
@@ -60,23 +61,62 @@ var BASIC_UPDATE_OMIT = {
60
61
  updatedAt: true
61
62
  };
62
63
  var queryListSchema = (schema) => import_zod.z.object({
63
- current: import_zod.z.string().optional().default("1"),
64
- // 默认页码为 1
65
- pageSize: import_zod.z.string().optional().default("10"),
66
- // 默认每页条数为 10
67
- createdAtFrom: import_zod.z.string().optional(),
68
- // 筛选开始日期
69
- createdAtTo: import_zod.z.string().optional(),
70
- // 筛选结束日期
71
- orderBy: import_zod.z.string().optional(),
72
- creatorId: import_zod.z.string().optional(),
73
- orderDir: import_zod.z.enum(["asc", "desc"]).optional()
64
+ current: import_zod.z.string().optional().default("1").describe("Page number, starting at 1."),
65
+ pageSize: import_zod.z.string().optional().default("10").describe(
66
+ "Rows per page. Omitting it yields only 10 — pass a larger value to get the whole set."
67
+ ),
68
+ createdAtFrom: import_zod.z.string().optional().describe(
69
+ "Keep only rows created at or after this instant (ISO 8601, e.g. 2026-08-01T00:00:00Z)."
70
+ ),
71
+ createdAtTo: import_zod.z.string().optional().describe("Keep only rows created at or before this instant."),
72
+ orderBy: import_zod.z.string().optional().describe(
73
+ "Column to sort by — a column name of this resource (createdAt / updatedAt and the like)."
74
+ ),
75
+ creatorId: import_zod.z.string().optional().describe(
76
+ "Keep only rows created by this user. Useful in shared spaces to filter down to one member."
77
+ ),
78
+ orderDir: import_zod.z.enum(["asc", "desc"]).optional().describe("Sort ascending or descending.")
74
79
  }).merge(schema);
75
80
  var listBodySchema = (schema) => import_zod.z.object({
76
81
  total: import_zod.z.number(),
77
82
  data: import_zod.z.array(schema)
78
83
  });
79
- var createTableSchema = ({ name, columns, serverColumns, extraConfig }) => {
84
+ var BASIC_FIELD_NOTES = {
85
+ creatorId: "User id of the creator. Stamped by the server; clients cannot send it.",
86
+ editorId: "User id of whoever changed this row last. Stamped by the server; clients cannot send it.",
87
+ id: "Primary key of this row (uuid)."
88
+ };
89
+ var BASIC_TIMESTAMP_NOTES = {
90
+ accessedAt: "When this row was last accessed.",
91
+ createdAt: "When this row was created.",
92
+ updatedAt: "When this row was last modified."
93
+ };
94
+ var describeFields = (schema, notes) => {
95
+ const overlay = {};
96
+ for (const [field, note] of Object.entries(notes)) {
97
+ const existing = schema.shape[field];
98
+ if (existing) {
99
+ overlay[field] = existing.describe(note);
100
+ }
101
+ }
102
+ return Object.keys(overlay).length ? schema.extend(overlay) : schema;
103
+ };
104
+ var describeBasicFields = (schema) => {
105
+ const withNotes = describeFields(schema, BASIC_FIELD_NOTES);
106
+ const overlay = {};
107
+ for (const [field, note] of Object.entries(BASIC_TIMESTAMP_NOTES)) {
108
+ const existing = withNotes.shape[field];
109
+ if (existing) {
110
+ overlay[field] = existing.meta({
111
+ description: note,
112
+ format: "date-time",
113
+ type: "string"
114
+ });
115
+ }
116
+ }
117
+ return Object.keys(overlay).length ? withNotes.extend(overlay) : withNotes;
118
+ };
119
+ var createTableSchema = ({ name, columns, serverColumns, extraConfig, describe }) => {
80
120
  const mergedColumns = {
81
121
  ...basicFields,
82
122
  ...serverColumns,
@@ -87,12 +127,26 @@ var createTableSchema = ({ name, columns, serverColumns, extraConfig }) => {
87
127
  mergedColumns,
88
128
  extraConfig
89
129
  );
90
- const selectSchema = (0, import_drizzle_zod.createSelectSchema)(table);
91
- const insertSchema = (0, import_drizzle_zod.createInsertSchema)((0, import_sqlite_core.sqliteTable)(name, columns));
92
- const updateSchema = (0, import_drizzle_zod.createUpdateSchema)(
93
- (0, import_sqlite_core.sqliteTable)(name, { id: (0, import_sqlite_core.text)("id"), ...columns })
94
- ).extend({ id: import_zod.z.string() });
95
- const querySchema = import_zod.z.object({ id: import_zod.z.string() });
130
+ const notes = describe ?? {};
131
+ const selectSchema = describeFields(
132
+ describeBasicFields((0, import_drizzle_zod.createSelectSchema)(table)),
133
+ notes
134
+ );
135
+ const insertSchema = describeFields(
136
+ (0, import_drizzle_zod.createInsertSchema)((0, import_sqlite_core.sqliteTable)(name, columns)),
137
+ notes
138
+ );
139
+ const updateSchema = describeFields(
140
+ (0, import_drizzle_zod.createUpdateSchema)(
141
+ (0, import_sqlite_core.sqliteTable)(name, { id: (0, import_sqlite_core.text)("id"), ...columns })
142
+ ).extend({
143
+ id: import_zod.z.string().describe("Id of the row to update.")
144
+ }),
145
+ notes
146
+ );
147
+ const querySchema = import_zod.z.object({
148
+ id: import_zod.z.string().describe("Id of the row to fetch.")
149
+ });
96
150
  const queryListWithSchema = queryListSchema(
97
151
  import_zod.z.object({}).catchall(import_zod.z.unknown())
98
152
  );
@@ -112,6 +166,7 @@ var createTableSchema = ({ name, columns, serverColumns, extraConfig }) => {
112
166
  BASIC_UPDATE_OMIT,
113
167
  basicFields,
114
168
  createTableSchema,
169
+ describeFields,
115
170
  listBodySchema,
116
171
  queryListSchema
117
172
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai0x0/utils",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI0x0 utils",
5
5
  "keywords": [
6
6
  "ai0x0"