@pylonts/dsl 1.1.11 → 1.1.13

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.
Files changed (50) hide show
  1. package/dist/convert.d.ts +6 -8
  2. package/dist/curd.js +1 -1
  3. package/dist/dao.d.ts +10 -7
  4. package/dist/dao.js +20 -7
  5. package/dist/dsl.d.ts +18 -1
  6. package/dist/dsl.js +40 -0
  7. package/dist/dto.d.ts +13 -8
  8. package/dist/dto.js +68 -13
  9. package/dist/entity.d.ts +4 -3
  10. package/dist/entity.js +1 -1
  11. package/dist/filter.d.ts +6 -4
  12. package/dist/filter.js +1 -1
  13. package/dist/flow-script.js +8 -2
  14. package/dist/flow.d.ts +10 -2
  15. package/dist/flow.js +44 -4
  16. package/dist/mermaid-driver.js +2 -2
  17. package/dist/project.d.ts +5 -2
  18. package/dist/project.js +21 -2
  19. package/dist/service.d.ts +13 -8
  20. package/dist/service.js +1 -1
  21. package/dist/third-service.d.ts +10 -53
  22. package/dist/third-service.js +3 -78
  23. package/dist/typebox-driver.d.ts +0 -6
  24. package/dist/typebox-driver.js +8 -36
  25. package/dist/utils.d.ts +2 -2
  26. package/docs/curd.md +55 -20
  27. package/docs/dao-generation.md +477 -477
  28. package/docs/project.md +32 -24
  29. package/docs/token.md +326 -326
  30. package/package.json +1 -1
  31. package/src/action.ts +51 -51
  32. package/src/controller.ts +53 -53
  33. package/src/convert.ts +76 -78
  34. package/src/curd.ts +104 -104
  35. package/src/dao.ts +504 -485
  36. package/src/dsl.ts +296 -257
  37. package/src/dto.ts +323 -266
  38. package/src/entity.ts +43 -42
  39. package/src/expr.ts +64 -64
  40. package/src/filter.ts +71 -69
  41. package/src/flow-script.ts +702 -695
  42. package/src/flow.ts +1272 -1226
  43. package/src/index.ts +46 -46
  44. package/src/mermaid-driver.ts +339 -339
  45. package/src/mysql-driver.ts +108 -108
  46. package/src/project.ts +138 -114
  47. package/src/service.ts +112 -107
  48. package/src/third-service.ts +68 -191
  49. package/src/typebox-driver.ts +234 -268
  50. package/src/utils.ts +74 -74
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/dist/service.d.ts CHANGED
@@ -1,17 +1,20 @@
1
- import type { SchemaBase } from './dsl.js';
1
+ import type { CollectionSchemaBase, SchemaBase } from './dsl.js';
2
2
  import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
3
  import type { DtoMessage } from './dto.js';
4
4
  import type { ExceptionSchema } from './exception.js';
5
5
  import type { FlowSchema } from './flow.js';
6
- /** A backend service serving exactly one frontend app (1:1 module). */
7
- export interface ServiceSchema extends SchemaBase {
6
+ /** A backend service serving one frontend app (1:1 module), or a
7
+ * platform-shared domain service (app unset — shared across modules). */
8
+ export interface ServiceSchema extends CollectionSchemaBase {
8
9
  type: 'service';
9
10
  /** The backend api module this service belongs to (shared instance from
10
11
  * project.config.ts apis). Services are always backend-side, so storage is
11
- * service_schema/{api.name}/{app.name}/service/. */
12
+ * service_schema/{api.name}/{app.name}/service/ — app unset = the api-level
13
+ * common domain layer, stored at service_schema/{api.name}/common/service/. */
12
14
  api: ProjectApiSchema;
13
- /** The frontend app this service serves (shared instance from project.config). */
14
- app: FrontAppSchema;
15
+ /** The frontend app this service serves (shared instance from project.config).
16
+ * Unset = api-level common domain service shared by all modules of the api. */
17
+ app?: FrontAppSchema;
15
18
  /** Methods keyed by name — the map key is written back as the method name. */
16
19
  methods: Record<string, ServiceMethodSchema>;
17
20
  }
@@ -20,11 +23,13 @@ export type ServiceMethodDef = Omit<ServiceMethodSchema, 'type' | 'schema' | 'na
20
23
  export declare function defineService(options: {
21
24
  name: string;
22
25
  api: ProjectApiSchema;
23
- app: FrontAppSchema;
26
+ app?: FrontAppSchema;
24
27
  methods: Record<string, ServiceMethodDef>;
25
28
  description?: string;
26
29
  }): ServiceSchema;
27
- /** A method exposed by a service. */
30
+ /** A method exposed by a business service — the contract with the calling
31
+ * frontend. Third-party integration methods are a separate contract
32
+ * (ThirdServiceMethodSchema): they can never bind a flow. */
28
33
  export interface ServiceMethodSchema extends SchemaBase {
29
34
  type: 'method';
30
35
  schema: ServiceSchema;
package/dist/service.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { exceptionEndNames } from './flow.js';
2
2
  export function defineService(options) {
3
- if (!options.api.apps.includes(options.app)) {
3
+ if (options.app && !options.api.apps.includes(options.app)) {
4
4
  throw new Error(`service ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
5
5
  }
6
6
  const schema = {
@@ -1,6 +1,6 @@
1
- import { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { DtoMessage } from './dto.js';
2
3
  import type { ExceptionSchema } from './exception.js';
3
- import type { FieldRuleEnd, FieldRuleSchema } from './field-rule.js';
4
4
  import type { ThirdApiSchema } from './project.js';
5
5
  /** A third-party integration service (e.g. tenpay wechat pay).
6
6
  * Distinct from ServiceSchema (backend service bound to an app) and
@@ -13,65 +13,22 @@ export interface ThirdServiceSchema extends SchemaBase {
13
13
  /** Methods keyed by name — the map key is written back as the method name. */
14
14
  methods: Record<string, ThirdServiceMethodSchema>;
15
15
  }
16
- /** A method exposed by a third-party service. */
16
+ /** A method of a third-party integration service. Same contract shape as
17
+ * ServiceMethodSchema minus flow — the implementation lives in the external
18
+ * system, there is nothing to model. Kept separate so a third method can
19
+ * never bind a flow. */
17
20
  export interface ThirdServiceMethodSchema extends SchemaBase {
18
21
  type: 'method';
19
22
  schema: ThirdServiceSchema;
20
- /** Input message — the Field-collection counterpart of a DtoMessage. */
21
- args: ThirdMethodSchema;
22
- /** Output message. */
23
- results: ThirdMethodSchema;
24
- /** Exceptions this method may throw (e.g. IOException, CodeException). */
25
- throws?: ExceptionSchema[];
26
- }
27
- /** Field binding to a rule: which end the local wire field stands on.
28
- * The ref always stands on the other end — from/to carry no extra information. */
29
- export interface ConvertFieldSchema {
30
- /** The rule binding field and ref (rule = name + two ends). */
31
- rule: FieldRuleSchema;
32
- /** The end instance the local wire field stands on. */
33
- end: FieldRuleEnd;
34
- }
35
- /** Same-fact variant link: a wire field carrying the same fact as a local
36
- * entity column under a different type/format (e.g. total_fee in fen vs amount in yuan). */
37
- export interface ThirdFieldRef {
38
- /** Wire field defined in this message (local definition). */
39
- field: Field;
40
- /** Field in another schema (table column or another message). */
41
- ref: Field;
42
- /** Optional rule binding — omitted when the fact is merely linked, not converted. */
43
- convert?: ConvertFieldSchema;
44
- }
45
- /** A directional message of a third-party method: a Field collection mirroring
46
- * TableSchema.columns, but fields hold wire-format names/types. A field may be
47
- * a shared instance of a local entity column (same fact, same type) — its
48
- * name/schema keep pointing at the table and the DTO projection inherits
49
- * type/semantics from the entity, exactly like from(table). */
50
- export interface ThirdMethodSchema extends CollectionSchemaBase {
51
- type: 'thirdMethod';
52
- /** The method this message belongs to (direction implied by args/results slot). */
53
- schema: ThirdServiceMethodSchema;
54
- /** Wire-format fields. */
55
- fields: Record<string, Field>;
56
- /** Same-fact variant links: wire field -> local entity column. */
57
- refs?: ThirdFieldRef[];
58
- }
59
- /** Message input for defineThirdMethod: type/schema are set by the builder. */
60
- export type ThirdMethodDef = Omit<ThirdMethodSchema, 'type' | 'schema'>;
61
- /** Build a third-party method message. Writes back name/schema on own fields
62
- * (top-level and nested); shared entity columns keep their table identity and
63
- * must be keyed by their column name. */
64
- export declare function defineThirdMethod(def: ThirdMethodDef): ThirdMethodSchema;
65
- /** Method input for defineThirdService: name is written back from the methods map key. */
66
- export interface ThirdServiceMethodDef {
67
23
  /** Input message. */
68
- args: ThirdMethodDef;
24
+ args: DtoMessage;
69
25
  /** Output message. */
70
- results: ThirdMethodDef;
26
+ results: DtoMessage;
71
27
  /** Exceptions this method may throw (e.g. IOException, CodeException). */
72
28
  throws?: ExceptionSchema[];
73
- description?: string;
74
29
  }
30
+ /** Method input for defineThirdService: name/schema are set by the builder. */
31
+ export type ThirdServiceMethodDef = Omit<ThirdServiceMethodSchema, 'type' | 'schema' | 'name'>;
75
32
  export declare function defineThirdService(options: {
76
33
  schema: ThirdApiSchema;
77
34
  name: string;
@@ -1,73 +1,3 @@
1
- /** Build a third-party method message. Writes back name/schema on own fields
2
- * (top-level and nested); shared entity columns keep their table identity and
3
- * must be keyed by their column name. */
4
- export function defineThirdMethod(def) {
5
- const message = {
6
- type: 'thirdMethod',
7
- name: def.name,
8
- description: def.description,
9
- // Filled by defineThirdService.
10
- schema: undefined,
11
- fields: def.fields,
12
- refs: def.refs,
13
- };
14
- for (const key of Object.keys(message.fields)) {
15
- const field = message.fields[key];
16
- if (field.schema === undefined) {
17
- field.name = key;
18
- field.schema = message;
19
- }
20
- else if (field.schema.type === 'table') {
21
- // Shared entity column: from() names the projection after field.name, so
22
- // a mismatched key would silently rename the wire field. Same-fact fields
23
- // with different names go through refs instead.
24
- if (field.name !== key) {
25
- throw new Error(`thirdMethod '${message.name}': shared column key '${key}' must match the column name '${field.name}' — ` +
26
- `same-fact fields with different names go through refs instead`);
27
- }
28
- }
29
- else if (field.schema !== message) {
30
- throw new Error(`thirdMethod '${message.name}': field '${key}' already belongs to ${field.schema.type} '${field.schema.name}', cannot reuse`);
31
- }
32
- writeBackNested(message, field);
33
- }
34
- if (message.refs !== undefined) {
35
- const ownFields = Object.values(message.fields);
36
- for (const link of message.refs) {
37
- if (!ownFields.includes(link.field)) {
38
- throw new Error(`thirdMethod '${message.name}': ref field must be one of its fields`);
39
- }
40
- if (link.ref.schema === undefined || link.ref.schema === message) {
41
- throw new Error(`thirdMethod '${message.name}': ref target '${link.ref.name}' must be defined in another schema`);
42
- }
43
- if (link.convert !== undefined) {
44
- const ends = Object.values(link.convert.rule.ends);
45
- if (!ends.includes(link.convert.end)) {
46
- throw new Error(`thirdMethod '${message.name}': convert end '${link.convert.end.name}' must be one of rule '${link.convert.rule.name}' ends`);
47
- }
48
- }
49
- }
50
- }
51
- return message;
52
- }
53
- /** Write back name/schema on nested wire fields (array items, object properties);
54
- * shared entity columns keep their table identity. */
55
- function writeBackNested(message, field) {
56
- if (field.type === 'array') {
57
- writeBackNested(message, field.items);
58
- return;
59
- }
60
- if (field.type !== 'object')
61
- return;
62
- for (const key of Object.keys(field.properties)) {
63
- const child = field.properties[key];
64
- if (child.schema === undefined) {
65
- child.name = key;
66
- child.schema = message;
67
- }
68
- writeBackNested(message, child);
69
- }
70
- }
71
1
  export function defineThirdService(options) {
72
2
  const schema = {
73
3
  type: 'thirdService',
@@ -78,20 +8,15 @@ export function defineThirdService(options) {
78
8
  };
79
9
  for (const key of Object.keys(options.methods)) {
80
10
  const method = options.methods[key];
81
- const methodSchema = {
11
+ schema.methods[key] = {
82
12
  type: 'method',
83
13
  name: key,
84
14
  description: method.description,
85
15
  schema,
86
- args: undefined,
87
- results: undefined,
16
+ args: method.args,
17
+ results: method.results,
88
18
  throws: method.throws,
89
19
  };
90
- methodSchema.args = defineThirdMethod(method.args);
91
- methodSchema.results = defineThirdMethod(method.results);
92
- methodSchema.args.schema = methodSchema;
93
- methodSchema.results.schema = methodSchema;
94
- schema.methods[key] = methodSchema;
95
20
  }
96
21
  return schema;
97
22
  }
@@ -1,5 +1,4 @@
1
1
  import { DtoMessage, ImportBase } from './dto.js';
2
- import type { ThirdMethodSchema } from './third-service.js';
3
2
  export type EnumResolver = (enumName: string) => ImportBase | undefined;
4
3
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
5
4
  export declare function collectDtoImports(schema: DtoMessage, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
@@ -11,8 +10,3 @@ export declare function renderDtoMessage(schema: DtoMessage, options?: {
11
10
  resolver?: EnumResolver;
12
11
  source?: string;
13
12
  }): string;
14
- /** Render one third-party method message export (const only — pair with
15
- * renderDtoTypeExport for the Static type). */
16
- export declare function renderThirdMethodExport(schema: ThirdMethodSchema, resolver: EnumResolver | undefined): string;
17
- /** Collect all imports needed to render a third-party method message: enum references. */
18
- export declare function collectThirdMethodImports(schema: ThirdMethodSchema, resolver: EnumResolver | undefined, out: Map<string, ImportBase>): void;
@@ -1,3 +1,5 @@
1
+ import { isDtoField, isDtoMessage } from './dto.js';
2
+ import { collectEnumRefs } from './dsl.js';
1
3
  function renderString(s) {
2
4
  return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
3
5
  }
@@ -116,18 +118,6 @@ function renderValue(f, indent, resolver) {
116
118
  // annotations; DB field defaults are not carried into the API contract.
117
119
  return renderBasic(f.field, f.pattern, f.default, resolver, indent);
118
120
  }
119
- /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
120
- function isDtoMessage(v) {
121
- if (typeof v !== 'object' || v === null)
122
- return false;
123
- return v.type === 'dto';
124
- }
125
- /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
126
- function isDtoField(v) {
127
- if (typeof v !== 'object' || v === null)
128
- return false;
129
- return 'field' in v && !('type' in v);
130
- }
131
121
  function collectEnumImports(f, resolver, out) {
132
122
  if (f.field.type === 'array') {
133
123
  const items = f.field.items;
@@ -154,23 +144,15 @@ function collectEnumImports(f, resolver, out) {
154
144
  }
155
145
  /** Enum import collection over a plain Field (wire-format nested fields). */
156
146
  function collectFieldEnumImports(field, resolver, out) {
157
- if (field.type === 'array') {
158
- collectFieldEnumImports(field.items, resolver, out);
159
- return;
160
- }
161
- if (field.type === 'object') {
162
- for (const child of Object.values(field.properties))
163
- collectFieldEnumImports(child, resolver, out);
164
- return;
147
+ for (const jsName of collectEnumRefs(field)) {
148
+ const ref = resolver?.(jsName);
149
+ if (!ref)
150
+ throw new Error(`enum ${jsName}: no import ref — pass an EnumResolver`);
151
+ out.set(`${ref.from}#${ref.name}`, ref);
165
152
  }
166
- if (field.type === 'enum')
167
- collectEnumRef(field, resolver, out);
168
153
  }
169
154
  function collectEnumRef(field, resolver, out) {
170
- const ref = resolver?.(field.enum.jsName);
171
- if (!ref)
172
- throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
173
- out.set(`${ref.from}#${ref.name}`, ref);
155
+ collectFieldEnumImports(field, resolver, out);
174
156
  }
175
157
  /** Collect all imports needed to render a DTO: include() bases + enum references. */
176
158
  export function collectDtoImports(schema, resolver, out) {
@@ -216,13 +198,3 @@ function renderBase(base) {
216
198
  const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
217
199
  return `${base.name}(${args.join(', ')})`;
218
200
  }
219
- /** Render one third-party method message export (const only — pair with
220
- * renderDtoTypeExport for the Static type). */
221
- export function renderThirdMethodExport(schema, resolver) {
222
- return `export const ${schema.name} = ${renderFieldObject(schema.fields, 1, resolver)};`;
223
- }
224
- /** Collect all imports needed to render a third-party method message: enum references. */
225
- export function collectThirdMethodImports(schema, resolver, out) {
226
- for (const f of Object.values(schema.fields))
227
- collectFieldEnumImports(f, resolver, out);
228
- }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Field, SchemaBase } from './dsl.js';
1
+ import type { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
2
2
  import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
3
  /** A utility method with a full signature. */
4
4
  export interface UtilsMethodSchema extends SchemaBase {
@@ -13,7 +13,7 @@ export interface UtilsMethodSchema extends SchemaBase {
13
13
  /** Method input for defineUtils: type/schema/name are set by the builder. */
14
14
  export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>;
15
15
  /** A base utility module (e.g. DateTimeUtils). */
16
- export interface UtilsSchema extends SchemaBase {
16
+ export interface UtilsSchema extends CollectionSchemaBase {
17
17
  type: 'utils';
18
18
  /** Backend binding — the api module this utils belongs to (shared instance
19
19
  * from project.config.ts apis). With `app` it serves that frontend
package/docs/curd.md CHANGED
@@ -10,10 +10,14 @@ CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD
10
10
 
11
11
  ```ts
12
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';
13
17
 
14
- export const orderCurd = defineCurd('order-curd', {
18
+ export const orderCurd = defineCurd('order', { // name = table.name 的 kebab(即 admin 路由路径)
15
19
  description: '订单管理',
16
- app: webAdmin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
20
+ app: admin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
17
21
  table: order, // 绑定实体表(共享实例)
18
22
  title: '订单管理',
19
23
  section: '订单管理', // 必填:sidebar 分组名
@@ -25,14 +29,35 @@ export const orderCurd = defineCurd('order-curd', {
25
29
  },
26
30
  list: {
27
31
  columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
28
- keyword: { columns: [order.columns.order_no] }, // 模糊搜索(本表字段)
32
+ filter: orderListFilter, // 搜索表单 + keyword(FilterSchema 引用,可选)
29
33
  orderBy: { column: order.columns.id, direction: 'desc' },
30
- searchFields: [{ field: order.columns.mer_id }, { field: order.columns.order_no, op: 'like' }],
31
34
  columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
32
35
  },
33
36
  });
34
37
  ```
35
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
+
36
61
  ## 字段
37
62
 
38
63
  | 字段 | 类型 | 说明 |
@@ -40,7 +65,7 @@ export const orderCurd = defineCurd('order-curd', {
40
65
  | `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
41
66
  | `table` | `TableSchema` | 绑定实体表(共享实例) |
42
67
  | `title` | `string` | 列表页中文标题 |
43
- | `section` | `string` | **必填**:sidebar 分组名(`pylonts gen curd` 据此生成路由注册的 `section` 字段) |
68
+ | `section` | `string` | **必填**:sidebar 分组名 |
44
69
  | `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
45
70
  | `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
46
71
  | `list` | `CurdListConfig` | 列表页配置(必填) |
@@ -56,31 +81,38 @@ export const orderCurd = defineCurd('order-curd', {
56
81
 
57
82
  | 字段 | 类型 | 说明 |
58
83
  |---|---|---|
59
- | `columns` | `Field[]` | 列表列,**必填非空**——前端要显示的字段必须全部显式列出;可含跨表字段(见下) |
60
- | `keyword?` | `{ columns: Field[] }` | 模糊搜索,columns 必须是**本表字段实例** |
84
+ | `columns` | `Field[]` | 列表列,**必填非空**;可含跨表字段 |
85
+ | `filter?` | `FilterSchema` | 页面过滤器引用:搜索表单(AND 条件)+ keyword(多列 OR 模糊);缺省 = 无搜索表单 |
61
86
  | `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
87
  | `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
64
88
 
65
- ## 跨表字段
89
+ ### FilterSchema(`defineFilter`)
66
90
 
67
- `columns` / `searchFields` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示关联表字段(如订单列表显示商户名称):
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 搜索) |
68
98
 
69
- ```ts
70
- list: {
71
- columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 跨表:字段指向 merchant.name
72
- }
73
- ```
99
+ ## 跨表字段
100
+
101
+ `list.columns` filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段(如订单列表显示商户名称、按商户名称过滤)。
74
102
 
75
103
  ## 默认与校验
76
104
 
77
- - `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)——前端显示什么必须显式定义
105
+ - `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)
78
106
  - `list.orderBy` **必填**,`column` 与 `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
79
107
  - 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
80
108
  - `app.type` 必须为 `'admin'`,否则抛错
109
+ - **`name` 必须是 `table.name` 的 kebab 形式**(name 即 admin 路由路径,不允许与所服务的表漂移)
110
+ - `section` 必填
81
111
  - 所有 `columns` 非空,否则抛错
82
- - `list.keyword.columns` / `list.orderBy.column` 必须属于 `table`,否则抛错
83
- - `list.columns` / `list.searchFields` 允许跨表,**不校验归属**
112
+ - `list.filter.app` 必须 === `curd.app`,否则抛错
113
+ - `list.orderBy.column` 必须属于 `table`,否则抛错
114
+ - `list.columns` 允许跨表,**不校验归属**
115
+ - 运行时校验(`defineFilter`):`api.apps` 包含 `app`;conditions 与 keyword 不能同时为空;keyword.columns 非空
84
116
  - 生成时校验(curd 生成器,`DtoSchemaGen.add` / `update`):
85
117
  - 表配置 `autoIncrement` 或 `generator`(主键由服务端生成)时,`actionPages.add.columns` **不允许包含主键字段**,否则抛错——AddRequest 不携带服务端生成的主键
86
118
  - `actionPages.update.columns` **必须包含主键字段**,否则抛错——UpdateRequest 靠主键定位记录
@@ -92,6 +124,9 @@ DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不
92
124
  | DTO | 命名 | 字段来源 |
93
125
  |---|---|---|
94
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) |
95
130
  | AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
96
131
  | UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
97
132
  | DetailRequest | `{Pascal}DetailRequest` | 主键 |
@@ -106,6 +141,6 @@ DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不
106
141
  | `operations: { label, action }` | `actions: ActionSchema[]` |
107
142
  | `detail.mode` 单例 | `actionPages.detail.mode` |
108
143
  | `forms.add / forms.update` | `actionPages.add / actionPages.update` |
109
- | `keyword` / `orderBy` / `columnTitles` | `list.keyword` / `list.orderBy` / `list.columnTitles`(列改字段实例引用) |
144
+ | `keyword` / `orderBy` / `columnTitles` | `list.filter`(FilterSchema)/ `list.orderBy` / `list.columnTitles` |
110
145
  | `naming` | 去掉(DTO 命名是生成器约定,非页面语义) |
111
- | DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |
146
+ | DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |