@ai0x0/utils 0.0.25 → 0.0.27

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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @ai0x0/utils
2
+
3
+ AI0x0 shared utilities — backend CRUD factories for Next.js App Router + drizzle-orm, and custom ESLint rules.
4
+
5
+ ## Backend (`src/backend/`)
6
+
7
+ Declarative CRUD factories on top of `next-rest-framework`, `drizzle-orm/pg-core`, `drizzle-zod`, and `zod`. Each factory returns a `RouteOperationDefinition` that plugs directly into `route({ … })`.
8
+
9
+ | Export | HTTP | Description |
10
+ | ------------------------ | ------ | ---------------------------------------- |
11
+ | `createPostOperation` | POST | Create a record |
12
+ | `createGetOperation` | GET | Get one record by filters |
13
+ | `createGetListOperation` | GET | Paginated list with filters, sort, joins |
14
+ | `createPutOperation` | PUT | Update a record |
15
+ | `createDeleteOperation` | DELETE | Delete a record |
16
+ | `createTableSchema` | — | Generate pg table + 5 Zod schemas |
17
+
18
+ **Built-in conventions:** base fields (`id`, `creatorId`, `editorId`, `createdAt`, `updatedAt`, `accessedAt`), ownership checks via `byCreator`, automatic date coercion, pagination (`current`/`pageSize`/`orderBy`/`orderDir`), list filtering with `ILIKE`, `IN`, `jsonb` array matching, and relation joins.
19
+
20
+ See [SKILL.md](./SKILL.md) for detailed workflow, setup, and escape hatches.
21
+
22
+ ## ESLint Rules (`eslint-rules/`)
23
+
24
+ Custom ESLint 9 flat-config rules ported from production projects. All enabled via `configs.recommended`:
25
+
26
+ ```js
27
+ // eslint.config.js
28
+ import localPlugin from "./eslint-rules/index.js";
29
+
30
+ export default [
31
+ localPlugin.configs.recommended,
32
+ // ... other config
33
+ ];
34
+ ```
35
+
36
+ | Rule | Description |
37
+ | ------------------------- | --------------------------------------- |
38
+ | `no-hardcoded-style` | Forbid hardcoded style values |
39
+ | `no-one-letter-vars` | Forbid single-letter variable names |
40
+ | `no-then` | Prefer `async/await` over `.then()` |
41
+ | `no-antd-space` | Use `<Flex>` instead of `<Space>` |
42
+ | `require-pro-components` | Require ProComponents usage |
43
+ | `require-section-divider` | Large files need `===` comment dividers |
44
+ | `no-consecutive-setstate` | Merge consecutive `setState` calls |
45
+ | `no-use-request-run` | Forbid `useRequest.run()` |
46
+ | `require-use-form` | Use `useForm()` for form state |
47
+ | `require-form-convention` | Form naming conventions |
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pnpm add @ai0x0/utils
53
+ ```
54
+
55
+ Peer dependencies: `drizzle-orm ^0.45.x`, `drizzle-zod ^0.8.x`, `zod ^4.x`.
56
+
57
+ ## Docs
58
+
59
+ - [SKILL.md](./SKILL.md) — full backend CRUD workflow and API reference
@@ -84,8 +84,8 @@ export function getListQuery(_ref) {
84
84
  }).map(function (_ref6) {
85
85
  var table = _ref6.table;
86
86
  return table;
87
- })) || []).find(function (t) {
88
- return t[key];
87
+ })) || []).find(function (table) {
88
+ return table[key];
89
89
  })) === null || _concat$find === void 0 ? void 0 : _concat$find[key];
90
90
  }
91
91
 
@@ -96,8 +96,8 @@ export function getListQuery(_ref) {
96
96
  if (typeof value === "string") {
97
97
  if (value.includes(",")) {
98
98
  // 处理多值
99
- var values = value.split(",").map(function (v) {
100
- return v.trim();
99
+ var values = value.split(",").map(function (val) {
100
+ return val.trim();
101
101
  }).filter(Boolean);
102
102
  if (values.length > 0) {
103
103
  if (isJsonArray) {
@@ -106,8 +106,8 @@ export function getListQuery(_ref) {
106
106
  } else if (isIdField) {
107
107
  conditions.push(inArray(targetColumn, values));
108
108
  } else {
109
- conditions.push(or.apply(void 0, _toConsumableArray(values.map(function (v) {
110
- return ilike(targetColumn, "%".concat(v, "%"));
109
+ conditions.push(or.apply(void 0, _toConsumableArray(values.map(function (val) {
110
+ return ilike(targetColumn, "%".concat(val, "%"));
111
111
  }))));
112
112
  }
113
113
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 本地 ESLint plugin:封装项目自定义规则。
3
+ */
4
+ import requireSectionDivider from "./require-section-divider.js";
5
+ import noHardcodedStyle from "./no-hardcoded-style.js";
6
+ import requireUseForm from "./require-use-form.js";
7
+ import requireFormConvention from "./require-form-convention.js";
8
+ import requireProComponents from "./require-pro-components.js";
9
+ import noAntdSpace from "./no-antd-space.js";
10
+ import noUseRequestRun from "./no-use-request-run.js";
11
+ import noThen from "./no-then.js";
12
+ import noOneLetterVars from "./no-one-letter-vars.js";
13
+ import noConsecutiveSetState from "./no-consecutive-setstate.js";
14
+
15
+ const rules = {
16
+ "require-section-divider": requireSectionDivider,
17
+ "no-hardcoded-style": noHardcodedStyle,
18
+ "require-use-form": requireUseForm,
19
+ "require-form-convention": requireFormConvention,
20
+ "require-pro-components": requireProComponents,
21
+ "no-antd-space": noAntdSpace,
22
+ "no-use-request-run": noUseRequestRun,
23
+ "no-then": noThen,
24
+ "no-one-letter-vars": noOneLetterVars,
25
+ "no-consecutive-setstate": noConsecutiveSetState,
26
+ };
27
+
28
+ const plugin = {
29
+ rules,
30
+ configs: {
31
+ recommended: {
32
+ plugins: { local: { rules } },
33
+ rules: Object.fromEntries(
34
+ Object.keys(rules).map((name) => [`local/${name}`, "error"]),
35
+ ),
36
+ },
37
+ },
38
+ };
39
+
40
+ export default plugin;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * no-antd-space
3
+ * -------------
4
+ * 禁止使用 antd <Space>,用 antd <Flex> 代替。
5
+ *
6
+ * 映射参考:
7
+ * - <Space direction="vertical"> → <Flex vertical>
8
+ * - <Space size={N}> → <Flex gap={N}>
9
+ * - <Space wrap> → <Flex wrap="wrap">
10
+ * - Space.Compact → 用 Flex 自行实现紧凑排列
11
+ *
12
+ * 本规则只负责检测,不负责自动修复。
13
+ */
14
+
15
+ /** @type {import('eslint').Rule.RuleModule} */
16
+ const rule = {
17
+ meta: {
18
+ type: "suggestion",
19
+ docs: {
20
+ description: "禁止使用 antd <Space>,请使用 antd <Flex> 代替。",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ useFlexInstead:
25
+ "禁止使用 antd <Space>,请使用 antd <Flex> 代替。<Space> 的 props 到 <Flex> 的映射:direction={vertical} → vertical, wrap → wrap='wrap', size={N} → gap={N}。",
26
+ useFlexInsteadCompact:
27
+ "禁止使用 antd <Space.Compact>,请使用 <Flex> 自行实现紧凑排列。",
28
+ },
29
+ },
30
+ create(context) {
31
+ const getTagName = (name) => {
32
+ if (name.type === "JSXIdentifier") return name.name;
33
+ if (
34
+ name.type === "JSXMemberExpression" &&
35
+ name.object.type === "JSXIdentifier" &&
36
+ name.property.type === "JSXIdentifier"
37
+ ) {
38
+ return `${name.object.name}.${name.property.name}`;
39
+ }
40
+ return "";
41
+ };
42
+
43
+ return {
44
+ JSXOpeningElement(node) {
45
+ const tagName = getTagName(node.name);
46
+ if (tagName === "Space") {
47
+ context.report({
48
+ node: node,
49
+ messageId: "useFlexInstead",
50
+ });
51
+ }
52
+ if (tagName === "Space.Compact") {
53
+ context.report({
54
+ node: node,
55
+ messageId: "useFlexInsteadCompact",
56
+ });
57
+ }
58
+ },
59
+ };
60
+ },
61
+ };
62
+
63
+ export default rule;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * no-consecutive-setstate
3
+ * ------------------------
4
+ * 禁止在同一个块中连续调用多个 setState()。
5
+ * ahooks useSetState 的 setState 会合并状态,连续多次调用应该合并为一次。
6
+ *
7
+ * 误:
8
+ * setState({ a: 1 });
9
+ * setState({ b: 2 });
10
+ *
11
+ * 正:
12
+ * setState({ a: 1, b: 2 });
13
+ */
14
+
15
+ /** @type {import('eslint').Rule.RuleModule} */
16
+ const rule = {
17
+ meta: {
18
+ type: "suggestion",
19
+ docs: {
20
+ description: "禁止连续调用多个 setState(),应合并为一次。",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ merge:
25
+ "连续多个 setState() 应合并为一次:将 {{prevField}} 和 {{currField}} 合并到一个 setState 中",
26
+ },
27
+ },
28
+ create(context) {
29
+ return {
30
+ BlockStatement(node) {
31
+ const body = node.body;
32
+ for (let i = 0; i + 1 < body.length; i++) {
33
+ const curr = body[i];
34
+ const next = body[i + 1];
35
+
36
+ if (
37
+ curr.type !== "ExpressionStatement" ||
38
+ next.type !== "ExpressionStatement"
39
+ )
40
+ continue;
41
+
42
+ const currExpr = curr.expression;
43
+ const nextExpr = next.expression;
44
+
45
+ const isCurrSetState =
46
+ currExpr.type === "CallExpression" &&
47
+ currExpr.callee.type === "Identifier" &&
48
+ currExpr.callee.name === "setState" &&
49
+ currExpr.arguments.length === 1 &&
50
+ currExpr.arguments[0].type === "ObjectExpression";
51
+
52
+ const isNextSetState =
53
+ nextExpr.type === "CallExpression" &&
54
+ nextExpr.callee.type === "Identifier" &&
55
+ nextExpr.callee.name === "setState" &&
56
+ nextExpr.arguments.length === 1 &&
57
+ nextExpr.arguments[0].type === "ObjectExpression";
58
+
59
+ if (!isCurrSetState || !isNextSetState) continue;
60
+
61
+ const prevField = currExpr.arguments[0];
62
+ const currField = nextExpr.arguments[0];
63
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
64
+ const prevFieldText = sourceCode
65
+ .getText(prevField)
66
+ .slice(1, -1)
67
+ .trim();
68
+ const currFieldText = sourceCode
69
+ .getText(currField)
70
+ .slice(1, -1)
71
+ .trim();
72
+
73
+ context.report({
74
+ node: next,
75
+ messageId: "merge",
76
+ data: {
77
+ prevField: prevFieldText,
78
+ currField: currFieldText,
79
+ },
80
+ });
81
+ }
82
+ },
83
+ };
84
+ },
85
+ };
86
+
87
+ export default rule;