@pylonts/dsl 1.1.11 → 1.1.12

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/dist/project.d.ts CHANGED
@@ -43,8 +43,11 @@ export interface ProjectSchema extends SchemaBase {
43
43
  * api.apps references the same instances from project.apps, so an app served
44
44
  * by multiple APIs is defined once and referenced many times.
45
45
  *
46
- * Runtime-validates app type whitelist, unique names and api.apps reference
47
- * integrity (same style as defineTable/defineCurd).
46
+ * Runtime-validates app type whitelist, unique names, api.apps reference
47
+ * integrity (same style as defineTable/defineCurd), plus two naming
48
+ * conventions: every app/api/thirdApi dir equals its name ('api/' == 'api'),
49
+ * and the first api must be named exactly 'api' (prefixed names like
50
+ * 'xx-api' are only allowed from the second api on).
48
51
  */
49
52
  export declare function defineProject(name: string, schema: {
50
53
  description?: string;
package/dist/project.js CHANGED
@@ -7,13 +7,26 @@ function checkInstanceName(project, kind, name) {
7
7
  throw new Error(`project ${project}: ${kind} name '${name}' must match ${INSTANCE_NAME_RE} (lowercase letters/digits/dashes; underscores are table-only)`);
8
8
  }
9
9
  }
10
+ // Directory convention: the instance dir equals its name ('api/' == 'api').
11
+ // One concept, one spelling — no separate dir/name pairs to keep in sync.
12
+ function normalizedDir(dir) {
13
+ return dir.replace(/[\\/]+$/, '');
14
+ }
15
+ function checkDirMatchesName(project, kind, name, dir) {
16
+ if (normalizedDir(dir) !== name) {
17
+ throw new Error(`project ${project}: ${kind} '${name}' dir must equal its name (got '${dir}')`);
18
+ }
19
+ }
10
20
  /**
11
21
  * Defines the project topology. FrontAppSchema instances are shared value objects:
12
22
  * api.apps references the same instances from project.apps, so an app served
13
23
  * by multiple APIs is defined once and referenced many times.
14
24
  *
15
- * Runtime-validates app type whitelist, unique names and api.apps reference
16
- * integrity (same style as defineTable/defineCurd).
25
+ * Runtime-validates app type whitelist, unique names, api.apps reference
26
+ * integrity (same style as defineTable/defineCurd), plus two naming
27
+ * conventions: every app/api/thirdApi dir equals its name ('api/' == 'api'),
28
+ * and the first api must be named exactly 'api' (prefixed names like
29
+ * 'xx-api' are only allowed from the second api on).
17
30
  */
18
31
  export function defineProject(name, schema) {
19
32
  const project = { name, ...schema, thirdApis: schema.thirdApis ?? [] };
@@ -30,6 +43,7 @@ export function defineProject(name, schema) {
30
43
  }
31
44
  if (!app.dir)
32
45
  throw new Error(`project ${name}: app '${app.name}' dir is required`);
46
+ checkDirMatchesName(name, 'app', app.name, app.dir);
33
47
  }
34
48
  const apiNames = new Set();
35
49
  for (const api of project.apis) {
@@ -41,18 +55,23 @@ export function defineProject(name, schema) {
41
55
  apiNames.add(api.name);
42
56
  if (!api.dir)
43
57
  throw new Error(`project ${name}: api '${api.name}' dir is required`);
58
+ checkDirMatchesName(name, 'api', api.name, api.dir);
44
59
  for (const ref of api.apps) {
45
60
  if (!project.apps.includes(ref)) {
46
61
  throw new Error(`project ${name}: api '${api.name}' references app '${ref.name}' that is not a shared instance in project.apps (define once and reference it)`);
47
62
  }
48
63
  }
49
64
  }
65
+ if (project.apis.length > 0 && project.apis[0].name !== 'api') {
66
+ throw new Error(`project ${name}: first api must be named 'api' (got '${project.apis[0].name}'); prefixed names like 'xx-api' are only allowed from the second api on`);
67
+ }
50
68
  for (const third of project.thirdApis) {
51
69
  if (!third.name)
52
70
  throw new Error(`project ${name}: thirdApi name is required`);
53
71
  checkInstanceName(name, 'thirdApi', third.name);
54
72
  if (!third.dir)
55
73
  throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
74
+ checkDirMatchesName(name, 'thirdApi', third.name, third.dir);
56
75
  }
57
76
  return project;
58
77
  }
package/docs/curd.md CHANGED
@@ -1,111 +1,146 @@
1
- # 管理端 CRUD 页面标准(CurdSchema)
2
-
3
- CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
4
-
5
- 页面定义文件按实体组织:`{project}/pages/{entity}.curd.ts`(与 `schema/*.table.ts` 平级)。
6
-
7
- **CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
8
-
9
- ## 定义
10
-
11
- ```ts
12
- import { defineCurd } from '@pylonts/dsl';
13
-
14
- export const orderCurd = defineCurd('order-curd', {
15
- description: '订单管理',
16
- app: webAdmin, // 所属管理端(project.config.ts FrontAppSchema 共享实例)
17
- table: order, // 绑定实体表(共享实例)
18
- title: '订单管理',
19
- section: '订单管理', // 必填:sidebar 分组名
20
- actions: [defineAction('EXPORT', '导出订单')], // 额外操作按钮
21
- actionPages: {
22
- add: { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
23
- update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
24
- detail: { mode: 'route', columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
25
- },
26
- list: {
27
- columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
28
- keyword: { columns: [order.columns.order_no] }, // 模糊搜索(本表字段)
29
- orderBy: { column: order.columns.id, direction: 'desc' },
30
- searchFields: [{ field: order.columns.mer_id }, { field: order.columns.order_no, op: 'like' }],
31
- columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
32
- },
33
- });
34
- ```
35
-
36
- ## 字段
37
-
38
- | 字段 | 类型 | 说明 |
39
- |---|---|---|
40
- | `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
41
- | `table` | `TableSchema` | 绑定实体表(共享实例) |
42
- | `title` | `string` | 列表页中文标题 |
43
- | `section` | `string` | **必填**:sidebar 分组名(`pylonts gen curd` 据此生成路由注册的 `section` 字段) |
44
- | `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
45
- | `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
46
- | `list` | `CurdListConfig` | 列表页配置(必填) |
47
-
48
- ### ActionPage
49
-
50
- | 字段 | 类型 | 说明 |
51
- |---|---|---|
52
- | `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
53
- | `columns` | `Field[]` | 该页面渲染的字段,**必填非空**——前端要显示的字段必须全部显式列出 |
54
-
55
- ### CurdListConfig
56
-
57
- | 字段 | 类型 | 说明 |
58
- |---|---|---|
59
- | `columns` | `Field[]` | 列表列,**必填非空**——前端要显示的字段必须全部显式列出;可含跨表字段(见下) |
60
- | `keyword?` | `{ columns: Field[] }` | 模糊搜索,columns 必须是**本表字段实例** |
61
- | `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序,**必填**,column 与 direction 都必填;column 必须是**本表字段实例** |
62
- | `searchFields?` | `{ field: Field; op?: Operator }[]` | 搜索条件字段,op 默认 `'eq'`,可选 `eq/gt/gte/lt/lte/like/ne` |
63
- | `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
64
-
65
- ## 跨表字段
66
-
67
- `columns` / `searchFields` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示关联表字段(如订单列表显示商户名称):
68
-
69
- ```ts
70
- list: {
71
- columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 跨表:字段指向 merchant.name
72
- }
73
- ```
74
-
75
- ## 默认与校验
76
-
77
- - `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)——前端显示什么必须显式定义
78
- - `list.orderBy` **必填**,`column` `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
79
- - 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
80
- - `app.type` 必须为 `'admin'`,否则抛错
81
- - 所有 `columns` 非空,否则抛错
82
- - `list.keyword.columns` / `list.orderBy.column` 必须属于 `table`,否则抛错
83
- - `list.columns` / `list.searchFields` 允许跨表,**不校验归属**
84
- - 生成时校验(curd 生成器,`DtoSchemaGen.add` / `update`):
85
- - 表配置 `autoIncrement` `generator`(主键由服务端生成)时,`actionPages.add.columns` **不允许包含主键字段**,否则抛错——AddRequest 不携带服务端生成的主键
86
- - `actionPages.update.columns` **必须包含主键字段**,否则抛错——UpdateRequest 靠主键定位记录
87
-
88
- ## DTO 推导(生成器约定)
89
-
90
- DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不持有 DTO 实例:
91
-
92
- | DTO | 命名 | 字段来源 |
93
- |---|---|---|
94
- | Row | `{Pascal}Row` | `list.columns` |
95
- | AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
96
- | UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
97
- | DetailRequest | `{Pascal}DetailRequest` | 主键 |
98
- | DetailResponse | `{Pascal}DetailResponse` | `actionPages.detail.columns` |
99
-
100
- ## 与旧 PageConfig 的差异
101
-
102
- | PageConfig(旧方案,已废弃) | CurdSchema |
103
- |---|---|
104
- | `module: string` | 由 `app` 推导(后端模块 == app 1:1) |
105
- | `schema: 'bd'` 字符串 | `table: TableSchema` 实例(类型安全) |
106
- | `operations: { label, action }` | `actions: ActionSchema[]` |
107
- | `detail.mode` 单例 | `actionPages.detail.mode` |
108
- | `forms.add / forms.update` | `actionPages.add / actionPages.update` |
109
- | `keyword` / `orderBy` / `columnTitles` | `list.keyword` / `list.orderBy` / `list.columnTitles`(列改字段实例引用) |
110
- | `naming` | 去掉(DTO 命名是生成器约定,非页面语义) |
111
- | DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |
1
+ # 管理端 CRUD 页面标准(CurdSchema)
2
+
3
+ CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
4
+
5
+ 页面定义文件按实体组织:`{project}/pages/{entity}.curd.ts`(与 `schema/*.table.ts` 平级)。
6
+
7
+ **CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
8
+
9
+ ## 定义
10
+
11
+ ```ts
12
+ import { defineCurd } from '@pylonts/dsl';
13
+ import { admin } from '../project.config';
14
+ import { order } from '../schema/order.table';
15
+ import { merchant } from '../schema/merchant.table';
16
+ import { orderListFilter } from '../filter_schema/api/admin/filter/order-list.filter';
17
+
18
+ export const orderCurd = defineCurd('order', { // name = table.name 的 kebab(即 admin 路由路径)
19
+ description: '订单管理',
20
+ app: admin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
21
+ table: order, // 绑定实体表(共享实例)
22
+ title: '订单管理',
23
+ section: '订单管理', // 必填:sidebar 分组名
24
+ actions: [defineAction('EXPORT', '导出订单')], // 额外操作按钮
25
+ actionPages: {
26
+ add: { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
27
+ update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
28
+ detail: { mode: 'route', columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
29
+ },
30
+ list: {
31
+ columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
32
+ filter: orderListFilter, // 搜索表单 + keyword(FilterSchema 引用,可选)
33
+ orderBy: { column: order.columns.id, direction: 'desc' },
34
+ columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
35
+ },
36
+ });
37
+ ```
38
+
39
+ 搜索条件**不内联在 list 里**,而是独立的 **FilterSchema**(`defineFilter`)声明,存放在 `filter_schema/{api.name}/{app.name}/filter/`(机器校验:一文件一 filter,文件名 = 名字去 Filter 后缀转 kebab):
40
+
41
+ ```ts
42
+ // filter_schema/api/admin/filter/order-list.filter.ts
43
+ import { defineFilter } from '@pylonts/dsl';
44
+ import { admin, api } from '../../../project.config';
45
+ import { order } from '../../../schema/order.table';
46
+
47
+ export const orderListFilter = defineFilter({
48
+ name: 'OrderListFilter',
49
+ api,
50
+ app: admin,
51
+ conditions: [
52
+ { field: order.columns.status, optional: true }, // op 默认 eq;optional = 有值才加 WHERE
53
+ { field: order.columns.order_no, op: 'like', optional: true },
54
+ ],
55
+ keyword: { columns: [order.columns.order_no] }, // 单输入值多列 OR 模糊
56
+ });
57
+ ```
58
+
59
+ 生成物为 `{api}/src/modules/{app}/filter/{FilterName}.ts`(两段柯里化 WHERE 拼装方法,DAO/Service 列表查询共用)。
60
+
61
+ ## 字段
62
+
63
+ | 字段 | 类型 | 说明 |
64
+ |---|---|---|
65
+ | `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
66
+ | `table` | `TableSchema` | 绑定实体表(共享实例) |
67
+ | `title` | `string` | 列表页中文标题 |
68
+ | `section` | `string` | **必填**:sidebar 分组名 |
69
+ | `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
70
+ | `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
71
+ | `list` | `CurdListConfig` | 列表页配置(必填) |
72
+
73
+ ### ActionPage
74
+
75
+ | 字段 | 类型 | 说明 |
76
+ |---|---|---|
77
+ | `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
78
+ | `columns` | `Field[]` | 该页面渲染的字段,**必填非空**——前端要显示的字段必须全部显式列出 |
79
+
80
+ ### CurdListConfig
81
+
82
+ | 字段 | 类型 | 说明 |
83
+ |---|---|---|
84
+ | `columns` | `Field[]` | 列表列,**必填非空**;可含跨表字段 |
85
+ | `filter?` | `FilterSchema` | 页面过滤器引用:搜索表单(AND 条件)+ keyword(多列 OR 模糊);缺省 = 无搜索表单 |
86
+ | `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序,**必填**,column 与 direction 都必填;column 必须是**本表字段实例** |
87
+ | `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
88
+
89
+ ### FilterSchema(`defineFilter`)
90
+
91
+ | 字段 | 类型 | 说明 |
92
+ |---|---|---|
93
+ | `name` | `string` | PascalCase、`Filter` 结尾;导出名 = name 首字母小写 |
94
+ | `api` | `ProjectApiSchema` | 所属后端 api(project.config.ts 共享实例);`api.apps` 必须包含 `app` |
95
+ | `app` | `FrontAppSchema` | 所属前端 app(共享实例);必须与引用它的 curd 同 app |
96
+ | `conditions?` | `FilterCondition[]` | AND 组合条件:`{ field, op?='eq', right?, optional? }`;`optional: true` = 有值才加 WHERE(页面搜索场景) |
97
+ | `keyword?` | `{ columns: Field[] }` | 单输入值对多列 OR like 模糊;配置后驱动「关键词查询」端点(`query({ keyword })`,供 Select/AutoComplete 搜索) |
98
+
99
+ ## 跨表字段
100
+
101
+ `list.columns` 与 filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段(如订单列表显示商户名称、按商户名称过滤)。
102
+
103
+ ## 默认与校验
104
+
105
+ - `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)
106
+ - `list.orderBy` **必填**,`column` `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
107
+ - 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
108
+ - `app.type` 必须为 `'admin'`,否则抛错
109
+ - **`name` 必须是 `table.name` kebab 形式**(name admin 路由路径,不允许与所服务的表漂移)
110
+ - `section` 必填
111
+ - 所有 `columns` 非空,否则抛错
112
+ - `list.filter.app` 必须 === `curd.app`,否则抛错
113
+ - `list.orderBy.column` 必须属于 `table`,否则抛错
114
+ - `list.columns` 允许跨表,**不校验归属**
115
+ - 运行时校验(`defineFilter`):`api.apps` 包含 `app`;conditions 与 keyword 不能同时为空;keyword.columns 非空
116
+ - 生成时校验(curd 生成器,`DtoSchemaGen.add` / `update`):
117
+ - 表配置 `autoIncrement` 或 `generator`(主键由服务端生成)时,`actionPages.add.columns` **不允许包含主键字段**,否则抛错——AddRequest 不携带服务端生成的主键
118
+ - `actionPages.update.columns` **必须包含主键字段**,否则抛错——UpdateRequest 靠主键定位记录
119
+
120
+ ## DTO 推导(生成器约定)
121
+
122
+ DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不持有 DTO 实例:
123
+
124
+ | DTO | 命名 | 字段来源 |
125
+ |---|---|---|
126
+ | Row | `{Pascal}Row` | `list.columns` |
127
+ | ListRequest | `{Pascal}ListRequest` | filter 的 conditions(camelCase + op)与 keyword + 分页参数(`PageRequest`,仅 paginated 表) |
128
+ | QueryRequest | `{Pascal}QueryRequest` | filter 的 conditions + keyword,无分页——keyword 查询端点专用(仅配置 keyword 时生成) |
129
+ | ListResponse | `{Pascal}ListResponse` | `PageResult(Row)`(仅 paginated 表;非分页表列表接口直接返回 `Row[]`,不生成 ListResponse) |
130
+ | AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
131
+ | UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
132
+ | DetailRequest | `{Pascal}DetailRequest` | 主键 |
133
+ | DetailResponse | `{Pascal}DetailResponse` | `actionPages.detail.columns` |
134
+
135
+ ## 与旧 PageConfig 的差异
136
+
137
+ | PageConfig(旧方案,已废弃) | CurdSchema |
138
+ |---|---|
139
+ | `module: string` | 由 `app` 推导(后端模块 == app 1:1) |
140
+ | `schema: 'bd'` 字符串 | `table: TableSchema` 实例(类型安全) |
141
+ | `operations: { label, action }` | `actions: ActionSchema[]` |
142
+ | `detail.mode` 单例 | `actionPages.detail.mode` |
143
+ | `forms.add / forms.update` | `actionPages.add / actionPages.update` |
144
+ | `keyword` / `orderBy` / `columnTitles` | `list.filter`(FilterSchema)/ `list.orderBy` / `list.columnTitles` |
145
+ | `naming` | 去掉(DTO 命名是生成器约定,非页面语义) |
146
+ | DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |
package/docs/project.md CHANGED
@@ -3,22 +3,30 @@
3
3
  Project 是仓库的地图:描述有哪些前端应用、哪些后端 API,以及每个 API 服务哪些前端。
4
4
 
5
5
  ```ts
6
- import { defineProject } from '@pylonts/dsl';
6
+ import { defineProject, FrontAppSchema, ProjectApiSchema } from '@pylonts/dsl';
7
7
 
8
- const webAdmin = { name: 'web-admin', type: 'admin', dir: 'web-admin/', description: '管理后台' };
9
- const miniUser = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
10
- const miniVerify = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/',description: '核销小程序' };
8
+ // Export-symbol rule: the export name equals the schema name kebab-camel
9
+ // (name 'admin' exports 'admin', 'mini-user' exports 'miniUser').
10
+ export const admin: FrontAppSchema = { name: 'admin', type: 'admin', dir: 'admin/', description: '管理后台' };
11
+ export const miniUser: FrontAppSchema = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
12
+ export const miniVerify: FrontAppSchema = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/', description: '核销小程序' };
13
+
14
+ // The first api must be named 'api'; a second api may use a prefixed name
15
+ // like 'xx-api' with dir 'xx-api/'.
16
+ export const api: ProjectApiSchema = { name: 'api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [admin, miniUser, miniVerify] };
11
17
 
12
18
  export const mall = defineProject('mall', {
13
19
  description: '合作商户权益兑换商城',
14
- apps: [webAdmin, miniUser, miniVerify],
15
- apis: [
16
- { name: 'mall-api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [webAdmin, miniUser, miniVerify] },
17
- ],
20
+ apps: [admin, miniUser, miniVerify],
21
+ apis: [api],
18
22
  });
19
23
  ```
20
24
 
21
25
  - `FrontAppSchema`:`name` / `description` / `type`(admin | wxmini)/ `dir`(相对仓库根目录的源码目录)。
22
26
  - `ProjectApiSchema`:`name` / `description` / `dir` / `apps`(直接引用共享的 FrontAppSchema 实例——一个 app 被多个 API 服务就定义一次、引用多次)/ `contextPath`(API 基础 URL 前缀,如 `/mall`,空串表示无前缀)。
23
27
  - **直接对象引用优先**:`api.apps` 与 `project.apps` 指向同一实例,不写字符串。
24
- - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
28
+ - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
29
+ - **命名约定(defineProject 运行时强制,违反即抛错)**:
30
+ - 每个 app / api / thirdApi 的 `dir` 必须等于 `name`(尾斜杠可有可无,`'api/'` == `'api'`)。
31
+ - `apis` 的第一个 api 必须命名为 `api`(导出符号即 `api`);带前缀的名字(如 `xx-api`)只允许从第二个 api 起。
32
+ - 实例导出符号 = 名字的 kebab-camel(`mini-user` → `miniUser`),loader 强制。
package/docs/token.md CHANGED
@@ -96,11 +96,11 @@ BLE 案例(business-ble)中,Java 侧 `Customer.java` 是 **POS 服务器
96
96
  ### 4.1 DSL 声明
97
97
 
98
98
  ```ts
99
- // token_schema/examples-api/admin/token/admin-user.token.ts
99
+ // token_schema/api/admin/token/admin-user.token.ts
100
100
  export const adminUserToken = defineToken({
101
101
  name: 'AdminUser',
102
102
  description: 'admin 后台登录主体',
103
- api: examplesApi, // 归属后端(模块双定:api + app)
103
+ api: api, // 归属后端(模块双定:api + app)
104
104
  app: admin,
105
105
  security: { // 安全材料段:签名、加密数据(未登录即有)
106
106
  ...from(apiKeyTable, [apiKeyTable.columns.app_key, apiKeyTable.columns.secret, apiKeyTable.columns.cipher]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/project.ts CHANGED
@@ -60,13 +60,28 @@ function checkInstanceName(project: string, kind: string, name: string): void {
60
60
  }
61
61
  }
62
62
 
63
+ // Directory convention: the instance dir equals its name ('api/' == 'api').
64
+ // One concept, one spelling — no separate dir/name pairs to keep in sync.
65
+ function normalizedDir(dir: string): string {
66
+ return dir.replace(/[\\/]+$/, '');
67
+ }
68
+
69
+ function checkDirMatchesName(project: string, kind: string, name: string, dir: string): void {
70
+ if (normalizedDir(dir) !== name) {
71
+ throw new Error(`project ${project}: ${kind} '${name}' dir must equal its name (got '${dir}')`);
72
+ }
73
+ }
74
+
63
75
  /**
64
76
  * Defines the project topology. FrontAppSchema instances are shared value objects:
65
77
  * api.apps references the same instances from project.apps, so an app served
66
78
  * by multiple APIs is defined once and referenced many times.
67
79
  *
68
- * Runtime-validates app type whitelist, unique names and api.apps reference
69
- * integrity (same style as defineTable/defineCurd).
80
+ * Runtime-validates app type whitelist, unique names, api.apps reference
81
+ * integrity (same style as defineTable/defineCurd), plus two naming
82
+ * conventions: every app/api/thirdApi dir equals its name ('api/' == 'api'),
83
+ * and the first api must be named exactly 'api' (prefixed names like
84
+ * 'xx-api' are only allowed from the second api on).
70
85
  */
71
86
  export function defineProject(
72
87
  name: string,
@@ -89,6 +104,7 @@ export function defineProject(
89
104
  throw new Error(`project ${name}: app '${app.name}' must be type 'admin', 'wxmini' or 'mobile' (got '${app.type}')`);
90
105
  }
91
106
  if (!app.dir) throw new Error(`project ${name}: app '${app.name}' dir is required`);
107
+ checkDirMatchesName(name, 'app', app.name, app.dir);
92
108
  }
93
109
 
94
110
  const apiNames = new Set<string>();
@@ -98,6 +114,7 @@ export function defineProject(
98
114
  if (apiNames.has(api.name)) throw new Error(`project ${name}: duplicate api name '${api.name}'`);
99
115
  apiNames.add(api.name);
100
116
  if (!api.dir) throw new Error(`project ${name}: api '${api.name}' dir is required`);
117
+ checkDirMatchesName(name, 'api', api.name, api.dir);
101
118
  for (const ref of api.apps) {
102
119
  if (!project.apps.includes(ref)) {
103
120
  throw new Error(`project ${name}: api '${api.name}' references app '${ref.name}' that is not a shared instance in project.apps (define once and reference it)`);
@@ -105,10 +122,17 @@ export function defineProject(
105
122
  }
106
123
  }
107
124
 
125
+ if (project.apis.length > 0 && project.apis[0].name !== 'api') {
126
+ throw new Error(
127
+ `project ${name}: first api must be named 'api' (got '${project.apis[0].name}'); prefixed names like 'xx-api' are only allowed from the second api on`,
128
+ );
129
+ }
130
+
108
131
  for (const third of project.thirdApis) {
109
132
  if (!third.name) throw new Error(`project ${name}: thirdApi name is required`);
110
133
  checkInstanceName(name, 'thirdApi', third.name);
111
134
  if (!third.dir) throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
135
+ checkDirMatchesName(name, 'thirdApi', third.name, third.dir);
112
136
  }
113
137
 
114
138
  return project;