@pylonts/dsl 1.1.5 → 1.1.6
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/controller.d.ts +27 -0
- package/dist/controller.js +11 -0
- package/dist/convert.d.ts +18 -6
- package/dist/convert.js +12 -2
- package/dist/curd.d.ts +0 -2
- package/dist/curd.js +7 -0
- package/dist/dao.d.ts +111 -2
- package/dist/dao.js +28 -2
- package/dist/db.js +3 -0
- package/dist/dsl.d.ts +16 -1
- package/dist/dsl.js +6 -0
- package/dist/dto.d.ts +6 -2
- package/dist/dto.js +20 -9
- package/dist/endpoint.d.ts +15 -0
- package/dist/endpoint.js +3 -0
- package/dist/exception.d.ts +14 -0
- package/dist/exception.js +9 -0
- package/dist/field-rule.d.ts +20 -0
- package/dist/field-rule.js +19 -0
- package/dist/flow.d.ts +24 -2
- package/dist/flow.js +16 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/mermaid-driver.js +32 -3
- package/dist/method.d.ts +11 -0
- package/dist/method.js +3 -0
- package/dist/mysql-driver.js +4 -0
- package/dist/provider.d.ts +6 -11
- package/dist/provider.js +2 -2
- package/dist/service.d.ts +16 -6
- package/dist/service.js +13 -2
- package/dist/third-service.d.ts +75 -0
- package/dist/third-service.js +96 -0
- package/dist/typebox-driver.d.ts +6 -0
- package/dist/typebox-driver.js +73 -12
- package/dist/utils.d.ts +25 -10
- package/dist/utils.js +28 -11
- package/docs/dto.md +73 -66
- package/docs/third-service.md +122 -0
- package/package.json +4 -1
- package/src/controller.ts +40 -0
- package/src/convert.ts +42 -15
- package/src/curd.ts +98 -93
- package/src/dao.ts +172 -13
- package/src/db.ts +186 -181
- package/src/dsl.ts +26 -1
- package/src/dto.ts +263 -247
- package/src/endpoint.ts +18 -0
- package/src/exception.ts +28 -0
- package/src/field-rule.ts +47 -0
- package/src/flow.ts +143 -103
- package/src/index.ts +43 -33
- package/src/mermaid-driver.ts +112 -84
- package/src/method.ts +20 -0
- package/src/mysql-driver.ts +4 -0
- package/src/provider.ts +67 -72
- package/src/service.ts +42 -20
- package/src/third-service.ts +186 -0
- package/src/typebox-driver.ts +82 -11
- package/src/utils.ts +63 -26
- package/dist/check-inheritance.d.ts +0 -9
- package/dist/check-inheritance.js +0 -58
package/src/dao.ts
CHANGED
|
@@ -1,13 +1,172 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import { FrontAppSchema } from './project.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { SchemaBase, Field, Operator } from './dsl.js';
|
|
2
|
+
import { FrontAppSchema } from './project.js';
|
|
3
|
+
import type { DtoMessage } from './dto.js';
|
|
4
|
+
import { TableSchema } from './db.js';
|
|
5
|
+
|
|
6
|
+
/** A data-access layer bound to exactly one frontend app. */
|
|
7
|
+
export interface DaoSchema extends SchemaBase {
|
|
8
|
+
type: 'dao';
|
|
9
|
+
/** The frontend app this DAO belongs to (shared instance from project.config). */
|
|
10
|
+
app: FrontAppSchema;
|
|
11
|
+
/** The table this DAO operates on (single-table atomicity). */
|
|
12
|
+
table: TableSchema;
|
|
13
|
+
/** Methods keyed by name — the map key is written back as the method name. */
|
|
14
|
+
methods: Record<string, DaoMethodSchema>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Method input for defineDao: name is written back from the methods map key. */
|
|
18
|
+
export type DaoMethodDef =
|
|
19
|
+
| Omit<FindSchema, 'schema' | 'name'>
|
|
20
|
+
| Omit<GetSchema, 'schema' | 'name'>
|
|
21
|
+
| Omit<InsertSchema, 'schema' | 'name'>
|
|
22
|
+
| Omit<UpdateSchema, 'schema' | 'name'>
|
|
23
|
+
| Omit<DeleteSchema, 'schema' | 'name'>
|
|
24
|
+
| Omit<AggregateSchema, 'schema' | 'name'>;
|
|
25
|
+
|
|
26
|
+
export function defineDao(options: {
|
|
27
|
+
name: string;
|
|
28
|
+
app: FrontAppSchema;
|
|
29
|
+
table: TableSchema;
|
|
30
|
+
methods: Record<string, DaoMethodDef>;
|
|
31
|
+
description?: string;
|
|
32
|
+
}): DaoSchema {
|
|
33
|
+
const schema: DaoSchema = {
|
|
34
|
+
type: 'dao',
|
|
35
|
+
name: options.name,
|
|
36
|
+
description: options.description,
|
|
37
|
+
app: options.app,
|
|
38
|
+
table: options.table,
|
|
39
|
+
methods: {},
|
|
40
|
+
};
|
|
41
|
+
for (const key of Object.keys(options.methods)) {
|
|
42
|
+
const method = options.methods[key] as DaoMethodDef;
|
|
43
|
+
// Spread of a union loses discriminant correlation; the cast is safe
|
|
44
|
+
// (the builder only adds name and the back-reference field).
|
|
45
|
+
schema.methods[key] = { ...method, name: key, schema } as DaoMethodSchema;
|
|
46
|
+
}
|
|
47
|
+
return schema;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// DAO method kinds. Signature only: name + params + result. No logic —
|
|
51
|
+
// complex SQL goes into description (text), never into schema.
|
|
52
|
+
|
|
53
|
+
/** One AND-combined criterion: a column plus its comparison operator. */
|
|
54
|
+
export interface QueryField {
|
|
55
|
+
/** Column to compare. */
|
|
56
|
+
field: Field;
|
|
57
|
+
/** Comparison operator; defaults to 'eq'. */
|
|
58
|
+
op?: Operator;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Query criteria for find methods. */
|
|
62
|
+
export interface QuerySchema extends SchemaBase {
|
|
63
|
+
type: 'query';
|
|
64
|
+
/** Criteria; all fields are AND-combined. */
|
|
65
|
+
fields: QueryField[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Sort specification for find results. */
|
|
69
|
+
export interface OrderBySchema {
|
|
70
|
+
/** Column to sort by. */
|
|
71
|
+
column: Field;
|
|
72
|
+
/** Sort direction. */
|
|
73
|
+
sort: 'asc' | 'desc';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Query rows by criteria; returns a list of row objects. */
|
|
77
|
+
export interface FindSchema extends SchemaBase {
|
|
78
|
+
type: 'find';
|
|
79
|
+
schema: DaoSchema;
|
|
80
|
+
/** Query criteria; omit = all rows. */
|
|
81
|
+
args?: QuerySchema;
|
|
82
|
+
/** Pagination marker: 'page' (page/pageSize) or 'limit' (position/limit).
|
|
83
|
+
* Only presence matters — parameter shapes are a generator convention. */
|
|
84
|
+
mode?: 'page' | 'limit';
|
|
85
|
+
/** Result sort; omit = no explicit order. */
|
|
86
|
+
orderBy?: OrderBySchema | OrderBySchema[];
|
|
87
|
+
/** Row type of the result list. */
|
|
88
|
+
results: EntitySchema;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Fetch a single row by primary key. Composite PK is not supported. */
|
|
92
|
+
export interface GetSchema extends SchemaBase {
|
|
93
|
+
type: 'get';
|
|
94
|
+
schema: DaoSchema;
|
|
95
|
+
/** Single PK scalar value. */
|
|
96
|
+
args: Field;
|
|
97
|
+
/** Row type. Get may miss the row: the generated signature returns Entity | null. */
|
|
98
|
+
results: EntitySchema;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Insert one row; returns number (insert id). */
|
|
102
|
+
export interface InsertSchema extends SchemaBase {
|
|
103
|
+
type: 'insert';
|
|
104
|
+
schema: DaoSchema;
|
|
105
|
+
/** Row object. */
|
|
106
|
+
args: EntitySchema;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Update one row; returns number (affected rows). The where key is derived
|
|
110
|
+
* from the PK columns inside args. */
|
|
111
|
+
export interface UpdateSchema extends SchemaBase {
|
|
112
|
+
type: 'update';
|
|
113
|
+
schema: DaoSchema;
|
|
114
|
+
/** Row object containing the PK columns plus the columns to set. */
|
|
115
|
+
args: EntitySchema;
|
|
116
|
+
/** Additional criteria beyond the derived PK (AND-combined). */
|
|
117
|
+
where?: QuerySchema;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Delete one row by primary key; returns number (affected rows).
|
|
121
|
+
* Composite PK is not supported. */
|
|
122
|
+
export interface DeleteSchema extends SchemaBase {
|
|
123
|
+
type: 'delete';
|
|
124
|
+
schema: DaoSchema;
|
|
125
|
+
/** Single PK scalar value. */
|
|
126
|
+
args: Field;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type DaoMethodSchema =
|
|
130
|
+
| FindSchema
|
|
131
|
+
| GetSchema
|
|
132
|
+
| InsertSchema
|
|
133
|
+
| UpdateSchema
|
|
134
|
+
| DeleteSchema
|
|
135
|
+
| AggregateSchema;
|
|
136
|
+
|
|
137
|
+
/** Aggregate expression result for aggregate queries. */
|
|
138
|
+
export interface ComputeExpr {
|
|
139
|
+
fn: 'sum' | 'avg' | 'count';
|
|
140
|
+
/** Column the function applies to; absent for count(*). */
|
|
141
|
+
field?: Field;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Aggregate expressions: Compute.sum(col) / Compute.avg(col) / Compute.count(). */
|
|
145
|
+
export const Compute = {
|
|
146
|
+
sum(field: Field): ComputeExpr {
|
|
147
|
+
return { fn: 'sum', field };
|
|
148
|
+
},
|
|
149
|
+
avg(field: Field): ComputeExpr {
|
|
150
|
+
return { fn: 'avg', field };
|
|
151
|
+
},
|
|
152
|
+
count(): ComputeExpr {
|
|
153
|
+
return { fn: 'count' };
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Aggregate query (count/sum/avg): returns computed scalar values. */
|
|
158
|
+
export interface AggregateSchema extends SchemaBase {
|
|
159
|
+
type: 'aggregate';
|
|
160
|
+
schema: DaoSchema;
|
|
161
|
+
/** Criteria, same shape as find. */
|
|
162
|
+
args?: QuerySchema;
|
|
163
|
+
/** Computed results: key = result field name. */
|
|
164
|
+
results: Record<string, ComputeExpr>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A database entity backed by a table. */
|
|
168
|
+
export interface EntitySchema extends SchemaBase {
|
|
169
|
+
type: 'entity';
|
|
170
|
+
/** The table columns of this entity. */
|
|
171
|
+
columns: Field[];
|
|
172
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -1,182 +1,187 @@
|
|
|
1
|
-
// DB table definitions.
|
|
2
|
-
// Shape: { type: 'table', <extension fields> }
|
|
3
|
-
|
|
4
|
-
import type { EntityPhrase } from './dictionary.js';
|
|
5
|
-
import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
|
|
6
|
-
|
|
7
|
-
export type Index = {
|
|
8
|
-
name?: string;
|
|
9
|
-
columns: Field | Field[];
|
|
10
|
-
unique?: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type ForeignKey = {
|
|
14
|
-
columns: Field | Field[];
|
|
15
|
-
references: Field | Field[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export interface TableSchemaOptions<
|
|
19
|
-
N extends string = string,
|
|
20
|
-
C extends Record<string, Field> = Record<string, Field>,
|
|
21
|
-
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
22
|
-
> {
|
|
23
|
-
description?: string;
|
|
24
|
-
paginated: boolean;
|
|
25
|
-
actor?: boolean;
|
|
26
|
-
generator?: string;
|
|
27
|
-
autoIncrement?: Field;
|
|
28
|
-
/** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
|
|
29
|
-
label?: Field;
|
|
30
|
-
primaryKey?: Field | Field[];
|
|
31
|
-
indexes?: Index[];
|
|
32
|
-
foreignKeys?: Record<string, ForeignKey>;
|
|
33
|
-
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
34
|
-
phrase?: EntityPhrase;
|
|
35
|
-
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
36
|
-
enums: E;
|
|
37
|
-
columns: C;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class TableSchema<
|
|
41
|
-
N extends string = string,
|
|
42
|
-
C extends Record<string, Field> = Record<string, Field>,
|
|
43
|
-
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
44
|
-
> implements CollectionSchemaBase {
|
|
45
|
-
type = 'table';
|
|
46
|
-
name: N;
|
|
47
|
-
description?: string;
|
|
48
|
-
/** 分页 */
|
|
49
|
-
paginated: boolean;
|
|
50
|
-
/** 系统操作者(如小程序为 C 端用户,管理端为运营) */
|
|
51
|
-
actor?: boolean;
|
|
52
|
-
/** id 生成器 */
|
|
53
|
-
generator?: string;
|
|
54
|
-
/** 自增主键字段 */
|
|
55
|
-
autoIncrement?: Field;
|
|
56
|
-
primaryKey?: Field | Field[];
|
|
57
|
-
indexes?: Index[];
|
|
58
|
-
/** 外键,引用其他表的字段 */
|
|
59
|
-
foreignKeys?: Record<string, ForeignKey>;
|
|
60
|
-
/** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
|
|
61
|
-
label?: Field;
|
|
62
|
-
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
63
|
-
phrase?: EntityPhrase;
|
|
64
|
-
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
65
|
-
enums: E;
|
|
66
|
-
columns: C;
|
|
67
|
-
|
|
68
|
-
constructor(name: N, options: TableSchemaOptions<N, C, E>) {
|
|
69
|
-
if (!options.enums) {
|
|
70
|
-
throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
|
|
71
|
-
}
|
|
72
|
-
if (options.paginated === undefined || options.paginated === null) {
|
|
73
|
-
throw new Error(`table '${name}': paginated is required — discuss with user whether this table needs pagination, then set paginated: true or paginated: false`);
|
|
74
|
-
}
|
|
75
|
-
this.name = name;
|
|
76
|
-
this.description = options.description;
|
|
77
|
-
this.paginated = options.paginated;
|
|
78
|
-
this.actor = options.actor;
|
|
79
|
-
this.generator = options.generator;
|
|
80
|
-
this.autoIncrement = options.autoIncrement;
|
|
81
|
-
this.primaryKey = options.primaryKey;
|
|
82
|
-
this.indexes = options.indexes;
|
|
83
|
-
this.foreignKeys = options.foreignKeys;
|
|
84
|
-
this.label = options.label;
|
|
85
|
-
this.phrase = options.phrase;
|
|
86
|
-
this.enums = options.enums;
|
|
87
|
-
this.columns = options.columns;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** True when the field is part of this table's primary key */
|
|
91
|
-
isPk(fieldRef: Field): boolean {
|
|
92
|
-
if (this.primaryKey === undefined) return false;
|
|
93
|
-
return Array.isArray(this.primaryKey)
|
|
94
|
-
? this.primaryKey.includes(fieldRef)
|
|
95
|
-
: this.primaryKey === fieldRef;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function defineTable<
|
|
100
|
-
N extends string,
|
|
101
|
-
C extends Record<string, Field>,
|
|
102
|
-
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
103
|
-
>(
|
|
104
|
-
name: N,
|
|
105
|
-
schema: TableSchemaOptions<N, C, E>,
|
|
106
|
-
): TableSchema<N, C, E> {
|
|
107
|
-
const table = new TableSchema<N, C, E>(name, schema);
|
|
108
|
-
if (table.generator && table.autoIncrement) {
|
|
109
|
-
throw new Error(`table '${name}': generator and autoIncrement are mutually exclusive`);
|
|
110
|
-
}
|
|
111
|
-
if (table.label && !Object.values(table.columns).includes(table.label)) {
|
|
112
|
-
throw new Error(`table '${name}': label field '${table.label.name}' must be one of the table's columns`);
|
|
113
|
-
}
|
|
114
|
-
for (const key of Object.keys(table.columns)) {
|
|
115
|
-
const field = table.columns[key] as Field;
|
|
116
|
-
if (field.schema && field.schema !== table) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1
|
+
// DB table definitions.
|
|
2
|
+
// Shape: { type: 'table', <extension fields> }
|
|
3
|
+
|
|
4
|
+
import type { EntityPhrase } from './dictionary.js';
|
|
5
|
+
import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
|
|
6
|
+
|
|
7
|
+
export type Index = {
|
|
8
|
+
name?: string;
|
|
9
|
+
columns: Field | Field[];
|
|
10
|
+
unique?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ForeignKey = {
|
|
14
|
+
columns: Field | Field[];
|
|
15
|
+
references: Field | Field[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface TableSchemaOptions<
|
|
19
|
+
N extends string = string,
|
|
20
|
+
C extends Record<string, Field> = Record<string, Field>,
|
|
21
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
22
|
+
> {
|
|
23
|
+
description?: string;
|
|
24
|
+
paginated: boolean;
|
|
25
|
+
actor?: boolean;
|
|
26
|
+
generator?: string;
|
|
27
|
+
autoIncrement?: Field;
|
|
28
|
+
/** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
|
|
29
|
+
label?: Field;
|
|
30
|
+
primaryKey?: Field | Field[];
|
|
31
|
+
indexes?: Index[];
|
|
32
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
33
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
34
|
+
phrase?: EntityPhrase;
|
|
35
|
+
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
36
|
+
enums: E;
|
|
37
|
+
columns: C;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TableSchema<
|
|
41
|
+
N extends string = string,
|
|
42
|
+
C extends Record<string, Field> = Record<string, Field>,
|
|
43
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
44
|
+
> implements CollectionSchemaBase {
|
|
45
|
+
type = 'table';
|
|
46
|
+
name: N;
|
|
47
|
+
description?: string;
|
|
48
|
+
/** 分页 */
|
|
49
|
+
paginated: boolean;
|
|
50
|
+
/** 系统操作者(如小程序为 C 端用户,管理端为运营) */
|
|
51
|
+
actor?: boolean;
|
|
52
|
+
/** id 生成器 */
|
|
53
|
+
generator?: string;
|
|
54
|
+
/** 自增主键字段 */
|
|
55
|
+
autoIncrement?: Field;
|
|
56
|
+
primaryKey?: Field | Field[];
|
|
57
|
+
indexes?: Index[];
|
|
58
|
+
/** 外键,引用其他表的字段 */
|
|
59
|
+
foreignKeys?: Record<string, ForeignKey>;
|
|
60
|
+
/** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
|
|
61
|
+
label?: Field;
|
|
62
|
+
/** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
|
|
63
|
+
phrase?: EntityPhrase;
|
|
64
|
+
/** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
|
|
65
|
+
enums: E;
|
|
66
|
+
columns: C;
|
|
67
|
+
|
|
68
|
+
constructor(name: N, options: TableSchemaOptions<N, C, E>) {
|
|
69
|
+
if (!options.enums) {
|
|
70
|
+
throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
|
|
71
|
+
}
|
|
72
|
+
if (options.paginated === undefined || options.paginated === null) {
|
|
73
|
+
throw new Error(`table '${name}': paginated is required — discuss with user whether this table needs pagination, then set paginated: true or paginated: false`);
|
|
74
|
+
}
|
|
75
|
+
this.name = name;
|
|
76
|
+
this.description = options.description;
|
|
77
|
+
this.paginated = options.paginated;
|
|
78
|
+
this.actor = options.actor;
|
|
79
|
+
this.generator = options.generator;
|
|
80
|
+
this.autoIncrement = options.autoIncrement;
|
|
81
|
+
this.primaryKey = options.primaryKey;
|
|
82
|
+
this.indexes = options.indexes;
|
|
83
|
+
this.foreignKeys = options.foreignKeys;
|
|
84
|
+
this.label = options.label;
|
|
85
|
+
this.phrase = options.phrase;
|
|
86
|
+
this.enums = options.enums;
|
|
87
|
+
this.columns = options.columns;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when the field is part of this table's primary key */
|
|
91
|
+
isPk(fieldRef: Field): boolean {
|
|
92
|
+
if (this.primaryKey === undefined) return false;
|
|
93
|
+
return Array.isArray(this.primaryKey)
|
|
94
|
+
? this.primaryKey.includes(fieldRef)
|
|
95
|
+
: this.primaryKey === fieldRef;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function defineTable<
|
|
100
|
+
N extends string,
|
|
101
|
+
C extends Record<string, Field>,
|
|
102
|
+
E extends Record<string, EnumDef> = Record<string, EnumDef>
|
|
103
|
+
>(
|
|
104
|
+
name: N,
|
|
105
|
+
schema: TableSchemaOptions<N, C, E>,
|
|
106
|
+
): TableSchema<N, C, E> {
|
|
107
|
+
const table = new TableSchema<N, C, E>(name, schema);
|
|
108
|
+
if (table.generator && table.autoIncrement) {
|
|
109
|
+
throw new Error(`table '${name}': generator and autoIncrement are mutually exclusive`);
|
|
110
|
+
}
|
|
111
|
+
if (table.label && !Object.values(table.columns).includes(table.label)) {
|
|
112
|
+
throw new Error(`table '${name}': label field '${table.label.name}' must be one of the table's columns`);
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(table.columns)) {
|
|
115
|
+
const field = table.columns[key] as Field;
|
|
116
|
+
if (field.schema && field.schema !== table) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (field.type === 'array' || field.type === 'object') {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`table '${name}': column '${key}' cannot be a nested ${field.type} field — wire-format nesting is not a table column`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const key of Object.keys(table.columns)) {
|
|
128
|
+
table.columns[key].name = key;
|
|
129
|
+
table.columns[key].schema = table;
|
|
130
|
+
}
|
|
131
|
+
if (table.primaryKey) {
|
|
132
|
+
const pkFields = Array.isArray(table.primaryKey) ? table.primaryKey : [table.primaryKey];
|
|
133
|
+
const colRefs = Object.values(table.columns);
|
|
134
|
+
for (const pk of pkFields) {
|
|
135
|
+
if (!colRefs.includes(pk)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`table '${name}': primaryKey field '${pk.name}' must be the exact same object as the corresponding column (extract it to a const and reuse)`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
|
|
143
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
144
|
+
const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
|
|
145
|
+
const colRefs = Object.values(table.columns);
|
|
146
|
+
for (const fkField of fields) {
|
|
147
|
+
if (!colRefs.includes(fkField)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`foreign key ${fkName}: column '${fkField.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < refs.length; i++) {
|
|
154
|
+
const ref = refs[i];
|
|
155
|
+
if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
|
|
156
|
+
if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
|
|
157
|
+
const phrase = (ref.schema as TableSchema).phrase;
|
|
158
|
+
if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
|
|
159
|
+
const expected = `${phrase.name}_${ref.name}`;
|
|
160
|
+
const fkField = fields[i];
|
|
161
|
+
if (fkField.name !== expected) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [idxKey, index] of Object.entries(table.indexes ?? {})) {
|
|
169
|
+
const cols = Array.isArray(index.columns) ? index.columns : [index.columns];
|
|
170
|
+
const colRefs = Object.values(table.columns);
|
|
171
|
+
for (const col of cols) {
|
|
172
|
+
if (typeof col !== 'object' || col === null || typeof (col as Field).type !== 'string') {
|
|
173
|
+
const idxName = index.name ?? idxKey;
|
|
174
|
+
throw new Error(
|
|
175
|
+
`index ${idxName}: columns must be Field instances (stringField()/intField()/...), got ${JSON.stringify(col)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!colRefs.includes(col)) {
|
|
179
|
+
const idxName = index.name ?? idxKey;
|
|
180
|
+
throw new Error(
|
|
181
|
+
`index ${idxName}: column '${col.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return table;
|
|
182
187
|
}
|
package/src/dsl.ts
CHANGED
|
@@ -143,6 +143,21 @@ interface JsonField extends BaseField {
|
|
|
143
143
|
jsType: 'object';
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** Recursive array field — wire-format nesting (third-party messages), not a table column. */
|
|
147
|
+
interface ArrayField extends BaseField {
|
|
148
|
+
type: 'array';
|
|
149
|
+
jsType: 'array';
|
|
150
|
+
/** Element type: any Field, including nested array/object. */
|
|
151
|
+
items: Field;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Recursive object field — wire-format nesting (third-party messages), not a table column. */
|
|
155
|
+
interface ObjectField extends BaseField {
|
|
156
|
+
type: 'object';
|
|
157
|
+
jsType: 'object';
|
|
158
|
+
properties: Record<string, Field>;
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
export type Field =
|
|
147
162
|
| StringField
|
|
148
163
|
| TextField
|
|
@@ -155,7 +170,9 @@ export type Field =
|
|
|
155
170
|
| TimeField
|
|
156
171
|
| DateTimeField
|
|
157
172
|
| EnumField
|
|
158
|
-
| JsonField
|
|
173
|
+
| JsonField
|
|
174
|
+
| ArrayField
|
|
175
|
+
| ObjectField;
|
|
159
176
|
|
|
160
177
|
// Field builders: type and jsType are fixed, pass extra properties only.
|
|
161
178
|
// The field name is written back from the map key later (see defineTable).
|
|
@@ -206,6 +223,14 @@ export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
|
|
|
206
223
|
return { name: '', type: 'json', jsType: 'object', ...extra };
|
|
207
224
|
}
|
|
208
225
|
|
|
226
|
+
export function arrayField(extra: FieldExtras<ArrayField>): ArrayField {
|
|
227
|
+
return { name: '', type: 'array', jsType: 'array', ...extra };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function objectField(extra: FieldExtras<ObjectField>): ObjectField {
|
|
231
|
+
return { name: '', type: 'object', jsType: 'object', ...extra };
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
|
|
210
235
|
const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
|
|
211
236
|
return { name: '', type: 'enum', jsType, ...extra };
|