@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
@@ -1,109 +1,109 @@
1
- import { Field, rateScale } from './dsl.js';
2
- import { ForeignKey, Index, TableSchema } from './db.js';
3
-
4
- // MySQL driver: converts a TableSchema into a CREATE TABLE statement.
5
-
6
- /** Default value constant for created_at columns. */
7
- export const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP';
8
- /** Default value constant for updated_at columns (with auto-update). */
9
- export const CURRENT_TIMESTAMP_ON_UPDATE = 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
10
-
11
- function columnType(field: Field): string {
12
- switch (field.type) {
13
- case 'string':
14
- if (!field.maxLength) throw new Error(`field ${field.name} (string) requires maxLength`);
15
- return `VARCHAR(${field.maxLength})`;
16
- case 'text':
17
- return 'TEXT';
18
- case 'integer':
19
- return 'INT';
20
- case 'bigint':
21
- return 'BIGINT';
22
- case 'decimal':
23
- return `DECIMAL(${field.precision}, ${field.scale})`;
24
- case 'rate':
25
- return `DECIMAL(5, ${rateScale(field.unit)})`;
26
- case 'boolean':
27
- return 'TINYINT(1)';
28
- case 'date':
29
- return 'DATE';
30
- case 'time':
31
- return 'TIME';
32
- case 'datetime':
33
- return 'DATETIME';
34
- case 'enum':
35
- // Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
36
- return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
37
- case 'json':
38
- return 'JSON';
39
- case 'array':
40
- case 'object':
41
- // Nested fields are wire-format only (third-party messages); table columns cannot nest.
42
- throw new Error(`field ${field.name} (${field.type}): nested fields are not supported on table columns`);
43
- case 'aggregate':
44
- // Aggregate fields are query outputs, never table columns.
45
- throw new Error(`field ${field.name} (aggregate): aggregate fields are not supported on table columns`);
46
- }
47
- }
48
-
49
- function renderDefault(field: Field): string {
50
- if (field.default === undefined) return '';
51
- // MySQL keyword expressions (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
52
- // NULL, etc.) — render bare, no quotes.
53
- if (/^[A-Z]/.test(field.default)) return ` DEFAULT ${field.default}`;
54
- // Numeric columns take a bare literal, not a quoted one.
55
- if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'rate' || field.type === 'boolean') {
56
- return ` DEFAULT ${field.default}`;
57
- }
58
- return ` DEFAULT '${field.default}'`;
59
- }
60
-
61
- function columnDef(field: Field, autoIncrement?: Field): string {
62
- const parts = [field.name, columnType(field)];
63
- if (field.optional === false) parts.push('NOT NULL');
64
- parts.push(renderDefault(field));
65
- if (field === autoIncrement) parts.push('AUTO_INCREMENT');
66
- if (field.description) parts.push(`COMMENT '${field.description.replace(/'/g, "\\'")}'`);
67
- return parts.join(' ');
68
- }
69
-
70
- function primaryKeyClause(schema: TableSchema): string | null {
71
- if (!schema.primaryKey) return null;
72
- const fields = Array.isArray(schema.primaryKey) ? schema.primaryKey : [schema.primaryKey];
73
- return `PRIMARY KEY (${fields.map((f) => f.name).join(', ')})`;
74
- }
75
-
76
- function indexClause(index: Index): string {
77
- const fields = Array.isArray(index.columns) ? index.columns : [index.columns];
78
- const kind = index.unique ? 'UNIQUE KEY' : 'KEY';
79
- const name = index.name ?? fields.map((f) => f.name).join('_');
80
- return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
81
- }
82
-
83
- function foreignKeyClause(name: string, fk: ForeignKey): string {
84
- const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
85
- const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
86
- const refTable = refs[0].schema?.name;
87
- if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`);
88
- const fkCols = fields.map((f) => f.name).join(', ');
89
- const refCols = refs.map((r) => r.name).join(', ');
90
- return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`;
91
- }
92
-
93
- export interface BuildCreateTableSqlOptions {
94
- /** 是否生成外键约束,默认 false(不生成) */
95
- generateForeignKeys?: boolean;
96
- }
97
-
98
- export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string {
99
- const lines = Object.values(schema.columns).map((field) => columnDef(field, schema.autoIncrement));
100
- const pk = primaryKeyClause(schema);
101
- if (pk) lines.push(pk);
102
- for (const index of schema.indexes ?? []) lines.push(indexClause(index));
103
- if (options.generateForeignKeys) {
104
- for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) {
105
- lines.push(foreignKeyClause(name, fk));
106
- }
107
- }
108
- return `CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
1
+ import { Field, rateScale } from './dsl.js';
2
+ import { ForeignKey, Index, TableSchema } from './db.js';
3
+
4
+ // MySQL driver: converts a TableSchema into a CREATE TABLE statement.
5
+
6
+ /** Default value constant for created_at columns. */
7
+ export const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP';
8
+ /** Default value constant for updated_at columns (with auto-update). */
9
+ export const CURRENT_TIMESTAMP_ON_UPDATE = 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
10
+
11
+ function columnType(field: Field): string {
12
+ switch (field.type) {
13
+ case 'string':
14
+ if (!field.maxLength) throw new Error(`field ${field.name} (string) requires maxLength`);
15
+ return `VARCHAR(${field.maxLength})`;
16
+ case 'text':
17
+ return 'TEXT';
18
+ case 'integer':
19
+ return 'INT';
20
+ case 'bigint':
21
+ return 'BIGINT';
22
+ case 'decimal':
23
+ return `DECIMAL(${field.precision}, ${field.scale})`;
24
+ case 'rate':
25
+ return `DECIMAL(5, ${rateScale(field.unit)})`;
26
+ case 'boolean':
27
+ return 'TINYINT(1)';
28
+ case 'date':
29
+ return 'DATE';
30
+ case 'time':
31
+ return 'TIME';
32
+ case 'datetime':
33
+ return 'DATETIME';
34
+ case 'enum':
35
+ // Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
36
+ return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
37
+ case 'json':
38
+ return 'JSON';
39
+ case 'array':
40
+ case 'object':
41
+ // Nested fields are wire-format only (third-party messages); table columns cannot nest.
42
+ throw new Error(`field ${field.name} (${field.type}): nested fields are not supported on table columns`);
43
+ case 'aggregate':
44
+ // Aggregate fields are query outputs, never table columns.
45
+ throw new Error(`field ${field.name} (aggregate): aggregate fields are not supported on table columns`);
46
+ }
47
+ }
48
+
49
+ function renderDefault(field: Field): string {
50
+ if (field.default === undefined) return '';
51
+ // MySQL keyword expressions (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
52
+ // NULL, etc.) — render bare, no quotes.
53
+ if (/^[A-Z]/.test(field.default)) return ` DEFAULT ${field.default}`;
54
+ // Numeric columns take a bare literal, not a quoted one.
55
+ if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'rate' || field.type === 'boolean') {
56
+ return ` DEFAULT ${field.default}`;
57
+ }
58
+ return ` DEFAULT '${field.default}'`;
59
+ }
60
+
61
+ function columnDef(field: Field, autoIncrement?: Field): string {
62
+ const parts = [field.name, columnType(field)];
63
+ if (field.optional === false) parts.push('NOT NULL');
64
+ parts.push(renderDefault(field));
65
+ if (field === autoIncrement) parts.push('AUTO_INCREMENT');
66
+ if (field.description) parts.push(`COMMENT '${field.description.replace(/'/g, "\\'")}'`);
67
+ return parts.join(' ');
68
+ }
69
+
70
+ function primaryKeyClause(schema: TableSchema): string | null {
71
+ if (!schema.primaryKey) return null;
72
+ const fields = Array.isArray(schema.primaryKey) ? schema.primaryKey : [schema.primaryKey];
73
+ return `PRIMARY KEY (${fields.map((f) => f.name).join(', ')})`;
74
+ }
75
+
76
+ function indexClause(index: Index): string {
77
+ const fields = Array.isArray(index.columns) ? index.columns : [index.columns];
78
+ const kind = index.unique ? 'UNIQUE KEY' : 'KEY';
79
+ const name = index.name ?? fields.map((f) => f.name).join('_');
80
+ return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
81
+ }
82
+
83
+ function foreignKeyClause(name: string, fk: ForeignKey): string {
84
+ const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
85
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
86
+ const refTable = refs[0].schema?.name;
87
+ if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`);
88
+ const fkCols = fields.map((f) => f.name).join(', ');
89
+ const refCols = refs.map((r) => r.name).join(', ');
90
+ return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`;
91
+ }
92
+
93
+ export interface BuildCreateTableSqlOptions {
94
+ /** 是否生成外键约束,默认 false(不生成) */
95
+ generateForeignKeys?: boolean;
96
+ }
97
+
98
+ export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string {
99
+ const lines = Object.values(schema.columns).map((field) => columnDef(field, schema.autoIncrement));
100
+ const pk = primaryKeyClause(schema);
101
+ if (pk) lines.push(pk);
102
+ for (const index of schema.indexes ?? []) lines.push(indexClause(index));
103
+ if (options.generateForeignKeys) {
104
+ for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) {
105
+ lines.push(foreignKeyClause(name, fk));
106
+ }
107
+ }
108
+ return `CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
109
109
  }
package/src/project.ts CHANGED
@@ -1,115 +1,139 @@
1
- import { SchemaBase } from './dsl.js';
2
- import type { TableSchema } from './db.js';
3
-
4
- // Project topology definitions: describe the applications (frontends) and
5
- // backend APIs of a repository, and which frontends each API serves.
6
-
7
- /** Frontend form factor. Closed enum, extend when new form factors appear. */
8
- export type FrontType = 'admin' | 'wxmini' | 'mobile';
9
-
10
- /** A frontend application (e.g. admin console, wechat mini program). */
11
- export interface FrontAppSchema extends SchemaBase {
12
- type: FrontType;
13
- /** Source directory relative to project root, e.g. 'web-admin/'. */
14
- dir: string;
15
- /**
16
- * Tenant table for this app. The tenant column of a business table is
17
- * deterministic: `{tenant.phrase}_{tenant.pk}` (e.g. shop with pk id →
18
- * `shop_id`). Tables carrying that column get automatic tenant scoping;
19
- * tables without it are global tables (e.g. system config) — both valid.
20
- */
21
- tenant?: TableSchema;
22
- }
23
-
24
- /** A backend API service. apps references shared FrontAppSchema instances. */
25
- export interface ProjectApiSchema extends SchemaBase {
26
- /** Source directory relative to project root, e.g. 'api/'. */
27
- dir: string;
28
- /** Frontends this API serves. Direct instance references (see defineProject). */
29
- apps: FrontAppSchema[];
30
- /** API base URL prefix shared by all apps it serves, e.g. '/mall'. '' = no prefix. */
31
- contextPath?: string;
32
- /** API service base URL for node clients, e.g. 'http://127.0.0.1:3000'. */
33
- baseUrl?: string;
34
- }
35
-
36
- /** A third-party system (e.g. wechat pay, unionpay). Owns its own
37
- * implementation dir and contract (controller_types), just like an API,
38
- * but is not part of this repo's served surface. */
39
- export interface ThirdApiSchema extends SchemaBase {
40
- /** Source directory relative to project root, e.g. 'wechat/'. */
41
- dir: string;
42
- }
43
-
44
- export interface ProjectSchema extends SchemaBase {
45
- apps: FrontAppSchema[];
46
- apis: ProjectApiSchema[];
47
- thirdApis: ThirdApiSchema[];
48
- }
49
-
50
- // Instance naming: lowercase letters/digits/dashes only. Underscores belong
51
- // to table names; the instance export symbol in project.config.ts must equal
52
- // the kebab-camel of the name, so 'admin_api' cannot map to a valid symbol.
53
- const INSTANCE_NAME_RE = /^[a-z][a-z0-9-]*$/;
54
-
55
- function checkInstanceName(project: string, kind: string, name: string): void {
56
- if (!INSTANCE_NAME_RE.test(name)) {
57
- throw new Error(
58
- `project ${project}: ${kind} name '${name}' must match ${INSTANCE_NAME_RE} (lowercase letters/digits/dashes; underscores are table-only)`,
59
- );
60
- }
61
- }
62
-
63
- /**
64
- * Defines the project topology. FrontAppSchema instances are shared value objects:
65
- * api.apps references the same instances from project.apps, so an app served
66
- * by multiple APIs is defined once and referenced many times.
67
- *
68
- * Runtime-validates app type whitelist, unique names and api.apps reference
69
- * integrity (same style as defineTable/defineCurd).
70
- */
71
- export function defineProject(
72
- name: string,
73
- schema: {
74
- description?: string;
75
- apps: FrontAppSchema[];
76
- apis: ProjectApiSchema[];
77
- thirdApis?: ThirdApiSchema[];
78
- },
79
- ): ProjectSchema {
80
- const project: ProjectSchema = { name, ...schema, thirdApis: schema.thirdApis ?? [] };
81
-
82
- const appNames = new Set<string>();
83
- for (const app of project.apps) {
84
- if (!app.name) throw new Error(`project ${name}: app name is required`);
85
- checkInstanceName(name, 'app', app.name);
86
- if (appNames.has(app.name)) throw new Error(`project ${name}: duplicate app name '${app.name}'`);
87
- appNames.add(app.name);
88
- if (app.type !== 'admin' && app.type !== 'wxmini' && app.type !== 'mobile') {
89
- throw new Error(`project ${name}: app '${app.name}' must be type 'admin', 'wxmini' or 'mobile' (got '${app.type}')`);
90
- }
91
- if (!app.dir) throw new Error(`project ${name}: app '${app.name}' dir is required`);
92
- }
93
-
94
- const apiNames = new Set<string>();
95
- for (const api of project.apis) {
96
- if (!api.name) throw new Error(`project ${name}: api name is required`);
97
- checkInstanceName(name, 'api', api.name);
98
- if (apiNames.has(api.name)) throw new Error(`project ${name}: duplicate api name '${api.name}'`);
99
- apiNames.add(api.name);
100
- if (!api.dir) throw new Error(`project ${name}: api '${api.name}' dir is required`);
101
- for (const ref of api.apps) {
102
- if (!project.apps.includes(ref)) {
103
- 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)`);
104
- }
105
- }
106
- }
107
-
108
- for (const third of project.thirdApis) {
109
- if (!third.name) throw new Error(`project ${name}: thirdApi name is required`);
110
- checkInstanceName(name, 'thirdApi', third.name);
111
- if (!third.dir) throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
112
- }
113
-
114
- return project;
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { TableSchema } from './db.js';
3
+
4
+ // Project topology definitions: describe the applications (frontends) and
5
+ // backend APIs of a repository, and which frontends each API serves.
6
+
7
+ /** Frontend form factor. Closed enum, extend when new form factors appear. */
8
+ export type FrontType = 'admin' | 'wxmini' | 'mobile';
9
+
10
+ /** A frontend application (e.g. admin console, wechat mini program). */
11
+ export interface FrontAppSchema extends SchemaBase {
12
+ type: FrontType;
13
+ /** Source directory relative to project root, e.g. 'web-admin/'. */
14
+ dir: string;
15
+ /**
16
+ * Tenant table for this app. The tenant column of a business table is
17
+ * deterministic: `{tenant.phrase}_{tenant.pk}` (e.g. shop with pk id →
18
+ * `shop_id`). Tables carrying that column get automatic tenant scoping;
19
+ * tables without it are global tables (e.g. system config) — both valid.
20
+ */
21
+ tenant?: TableSchema;
22
+ }
23
+
24
+ /** A backend API service. apps references shared FrontAppSchema instances. */
25
+ export interface ProjectApiSchema extends SchemaBase {
26
+ /** Source directory relative to project root, e.g. 'api/'. */
27
+ dir: string;
28
+ /** Frontends this API serves. Direct instance references (see defineProject). */
29
+ apps: FrontAppSchema[];
30
+ /** API base URL prefix shared by all apps it serves, e.g. '/mall'. '' = no prefix. */
31
+ contextPath?: string;
32
+ /** API service base URL for node clients, e.g. 'http://127.0.0.1:3000'. */
33
+ baseUrl?: string;
34
+ }
35
+
36
+ /** A third-party system (e.g. wechat pay, unionpay). Owns its own
37
+ * implementation dir and contract (controller_types), just like an API,
38
+ * but is not part of this repo's served surface. */
39
+ export interface ThirdApiSchema extends SchemaBase {
40
+ /** Source directory relative to project root, e.g. 'wechat/'. */
41
+ dir: string;
42
+ }
43
+
44
+ export interface ProjectSchema extends SchemaBase {
45
+ apps: FrontAppSchema[];
46
+ apis: ProjectApiSchema[];
47
+ thirdApis: ThirdApiSchema[];
48
+ }
49
+
50
+ // Instance naming: lowercase letters/digits/dashes only. Underscores belong
51
+ // to table names; the instance export symbol in project.config.ts must equal
52
+ // the kebab-camel of the name, so 'admin_api' cannot map to a valid symbol.
53
+ const INSTANCE_NAME_RE = /^[a-z][a-z0-9-]*$/;
54
+
55
+ function checkInstanceName(project: string, kind: string, name: string): void {
56
+ if (!INSTANCE_NAME_RE.test(name)) {
57
+ throw new Error(
58
+ `project ${project}: ${kind} name '${name}' must match ${INSTANCE_NAME_RE} (lowercase letters/digits/dashes; underscores are table-only)`,
59
+ );
60
+ }
61
+ }
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
+
75
+ /**
76
+ * Defines the project topology. FrontAppSchema instances are shared value objects:
77
+ * api.apps references the same instances from project.apps, so an app served
78
+ * by multiple APIs is defined once and referenced many times.
79
+ *
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).
85
+ */
86
+ export function defineProject(
87
+ name: string,
88
+ schema: {
89
+ description?: string;
90
+ apps: FrontAppSchema[];
91
+ apis: ProjectApiSchema[];
92
+ thirdApis?: ThirdApiSchema[];
93
+ },
94
+ ): ProjectSchema {
95
+ const project: ProjectSchema = { name, ...schema, thirdApis: schema.thirdApis ?? [] };
96
+
97
+ const appNames = new Set<string>();
98
+ for (const app of project.apps) {
99
+ if (!app.name) throw new Error(`project ${name}: app name is required`);
100
+ checkInstanceName(name, 'app', app.name);
101
+ if (appNames.has(app.name)) throw new Error(`project ${name}: duplicate app name '${app.name}'`);
102
+ appNames.add(app.name);
103
+ if (app.type !== 'admin' && app.type !== 'wxmini' && app.type !== 'mobile') {
104
+ throw new Error(`project ${name}: app '${app.name}' must be type 'admin', 'wxmini' or 'mobile' (got '${app.type}')`);
105
+ }
106
+ if (!app.dir) throw new Error(`project ${name}: app '${app.name}' dir is required`);
107
+ checkDirMatchesName(name, 'app', app.name, app.dir);
108
+ }
109
+
110
+ const apiNames = new Set<string>();
111
+ for (const api of project.apis) {
112
+ if (!api.name) throw new Error(`project ${name}: api name is required`);
113
+ checkInstanceName(name, 'api', api.name);
114
+ if (apiNames.has(api.name)) throw new Error(`project ${name}: duplicate api name '${api.name}'`);
115
+ apiNames.add(api.name);
116
+ if (!api.dir) throw new Error(`project ${name}: api '${api.name}' dir is required`);
117
+ checkDirMatchesName(name, 'api', api.name, api.dir);
118
+ for (const ref of api.apps) {
119
+ if (!project.apps.includes(ref)) {
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)`);
121
+ }
122
+ }
123
+ }
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
+
131
+ for (const third of project.thirdApis) {
132
+ if (!third.name) throw new Error(`project ${name}: thirdApi name is required`);
133
+ checkInstanceName(name, 'thirdApi', third.name);
134
+ if (!third.dir) throw new Error(`project ${name}: thirdApi '${third.name}' dir is required`);
135
+ checkDirMatchesName(name, 'thirdApi', third.name, third.dir);
136
+ }
137
+
138
+ return project;
115
139
  }