@tailor-platform/erp-kit 0.1.1 → 0.1.2
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/CHANGELOG.md +6 -0
- package/README.md +77 -50
- package/dist/cli.js +691 -571
- package/package.json +1 -1
- package/schemas/module/command.yml +1 -0
- package/schemas/module/model.yml +9 -0
- package/schemas/module/query.yml +53 -0
- package/src/cli.ts +6 -88
- package/src/commands/app/index.ts +74 -0
- package/src/commands/check.test.ts +2 -1
- package/src/commands/check.ts +1 -0
- package/src/commands/module/index.ts +85 -0
- package/src/commands/module/list.test.ts +62 -0
- package/src/commands/module/list.ts +64 -0
- package/src/commands/scaffold.test.ts +5 -0
- package/src/commands/scaffold.ts +2 -1
- package/src/commands/sync-check.test.ts +28 -0
- package/src/commands/sync-check.ts +6 -0
- package/src/integration.test.ts +6 -8
- package/src/module.ts +4 -3
- package/src/modules/primitives/docs/models/Currency.md +4 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
- package/src/modules/primitives/docs/models/Unit.md +4 -1
- package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
- package/src/modules/primitives/index.ts +2 -2
- package/src/modules/primitives/module.ts +5 -3
- package/src/modules/primitives/permissions.ts +0 -2
- package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
- package/src/modules/primitives/query/convertAmount.ts +122 -0
- package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
- package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
- package/src/modules/shared/defineQuery.test.ts +28 -0
- package/src/modules/shared/defineQuery.ts +16 -0
- package/src/modules/shared/internal.ts +2 -1
- package/src/modules/shared/types.ts +8 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
- package/src/modules/user-management/docs/models/Permission.md +2 -0
- package/src/modules/user-management/docs/models/Role.md +2 -0
- package/src/modules/user-management/docs/models/RolePermission.md +2 -0
- package/src/modules/user-management/docs/models/User.md +2 -0
- package/src/modules/user-management/docs/models/UserRole.md +2 -0
- package/src/schemas.ts +1 -0
- package/src/modules/primitives/command/convertAmount.ts +0 -126
- /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
- /package/src/modules/primitives/docs/{commands → queries}/ConvertQuantity.md +0 -0
package/package.json
CHANGED
|
@@ -35,6 +35,7 @@ structure:
|
|
|
35
35
|
# Error handling - REQUIRED
|
|
36
36
|
# How errors are handled
|
|
37
37
|
- heading: "## Error Scenarios"
|
|
38
|
+
description: "Use UPPER_SNAKE_CASE error codes to identify each scenario.\ne.g. `- **ITEM_NOT_FOUND**: Specified item ID does not exist`"
|
|
38
39
|
lists:
|
|
39
40
|
- min: 0
|
|
40
41
|
type: unordered
|
package/schemas/module/model.yml
CHANGED
|
@@ -37,6 +37,15 @@ structure:
|
|
|
37
37
|
type: unordered
|
|
38
38
|
min_items: 0
|
|
39
39
|
|
|
40
|
+
- heading: "### Query Definitions"
|
|
41
|
+
description: |
|
|
42
|
+
Definitions of queries this domain model supports.
|
|
43
|
+
Should match with the queries defined in modules/**/docs/queries/*.md and link to them.
|
|
44
|
+
lists:
|
|
45
|
+
- min: 0
|
|
46
|
+
type: unordered
|
|
47
|
+
min_items: 0
|
|
48
|
+
|
|
40
49
|
- heading: "### Models"
|
|
41
50
|
description: |
|
|
42
51
|
Actual database models necessary for the domain model.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/jackchuka/mdschema/main/schema.json
|
|
2
|
+
|
|
3
|
+
structure:
|
|
4
|
+
- heading:
|
|
5
|
+
expr: "filename == heading"
|
|
6
|
+
children:
|
|
7
|
+
# What this query does - REQUIRED
|
|
8
|
+
- heading: "## Overview"
|
|
9
|
+
|
|
10
|
+
# Business rules - REQUIRED
|
|
11
|
+
# Rules, constraints, and validation logic
|
|
12
|
+
- heading: "## Business Rules"
|
|
13
|
+
allow_additional: true
|
|
14
|
+
lists:
|
|
15
|
+
- min: 0
|
|
16
|
+
type: unordered
|
|
17
|
+
min_items: 1
|
|
18
|
+
|
|
19
|
+
# How it works - REQUIRED
|
|
20
|
+
# Step-by-step flow or workflow
|
|
21
|
+
- heading: "## Process Flow"
|
|
22
|
+
code_blocks:
|
|
23
|
+
- lang: mermaid
|
|
24
|
+
min: 1
|
|
25
|
+
|
|
26
|
+
# External dependencies - OPTIONAL
|
|
27
|
+
# Queries from other modules that this query depends on
|
|
28
|
+
- heading: "## External Dependencies"
|
|
29
|
+
description: "e.g. `- [inventory::getStockLevel](../../inventory/queries/getStockLevel.md) - Get current stock level`"
|
|
30
|
+
lists:
|
|
31
|
+
- min: 0
|
|
32
|
+
type: unordered
|
|
33
|
+
min_items: 1
|
|
34
|
+
|
|
35
|
+
# Error handling - REQUIRED
|
|
36
|
+
# How errors are handled
|
|
37
|
+
- heading: "## Error Scenarios"
|
|
38
|
+
description: "Use UPPER_SNAKE_CASE error codes to identify each scenario.\ne.g. `- **ITEM_NOT_FOUND**: Specified item ID does not exist`"
|
|
39
|
+
lists:
|
|
40
|
+
- min: 0
|
|
41
|
+
type: unordered
|
|
42
|
+
min_items: 1
|
|
43
|
+
|
|
44
|
+
# Link validation
|
|
45
|
+
links:
|
|
46
|
+
validate_internal: true
|
|
47
|
+
validate_files: true
|
|
48
|
+
|
|
49
|
+
# Heading rules
|
|
50
|
+
heading_rules:
|
|
51
|
+
no_skip_levels: true
|
|
52
|
+
unique: true
|
|
53
|
+
max_depth: 4
|
package/src/cli.ts
CHANGED
|
@@ -2,94 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { defineCommand, runMain, arg } from "politty";
|
|
5
|
-
import { runCheck } from "./commands/check.js";
|
|
6
|
-
import { runSyncCheck, formatSyncCheckReport } from "./commands/sync-check.js";
|
|
7
|
-
import { runScaffold, ALL_TYPES, isModuleType, type ScaffoldType } from "./commands/scaffold.js";
|
|
8
5
|
import { runInit } from "./commands/init.js";
|
|
9
6
|
import { mockCommand } from "./commands/mock/index.js";
|
|
7
|
+
import { moduleCommand } from "./commands/module/index.js";
|
|
8
|
+
import { appCommand } from "./commands/app/index.js";
|
|
10
9
|
|
|
11
10
|
const cwd = process.cwd();
|
|
12
11
|
|
|
13
|
-
const rootArgs = z.object({
|
|
14
|
-
modulesRoot: arg(z.string().optional(), {
|
|
15
|
-
alias: "m",
|
|
16
|
-
description: "Path to modules directory",
|
|
17
|
-
}),
|
|
18
|
-
appRoot: arg(z.string().optional(), {
|
|
19
|
-
alias: "a",
|
|
20
|
-
description: "Path to app-compose directory (apps/ or examples/)",
|
|
21
|
-
}),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function requireRoot(args: { modulesRoot?: string; appRoot?: string }) {
|
|
25
|
-
const paths = { modulesRoot: args.modulesRoot, appRoot: args.appRoot };
|
|
26
|
-
if (!paths.modulesRoot && !paths.appRoot) {
|
|
27
|
-
console.error("At least one of --modules-root or --app-root is required.");
|
|
28
|
-
process.exit(2);
|
|
29
|
-
}
|
|
30
|
-
return paths;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const checkCommand = defineCommand({
|
|
34
|
-
name: "check",
|
|
35
|
-
description: "Validate docs against schemas",
|
|
36
|
-
args: rootArgs,
|
|
37
|
-
run: async (args) => {
|
|
38
|
-
const paths = requireRoot(args);
|
|
39
|
-
const exitCode = await runCheck(paths, cwd);
|
|
40
|
-
process.exit(exitCode);
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const syncCheckCommand = defineCommand({
|
|
45
|
-
name: "sync-check",
|
|
46
|
-
description: "Validate source <-> doc correspondence",
|
|
47
|
-
args: rootArgs,
|
|
48
|
-
run: async (args) => {
|
|
49
|
-
const paths = requireRoot(args);
|
|
50
|
-
const result = await runSyncCheck(paths, cwd);
|
|
51
|
-
console.log(formatSyncCheckReport(result));
|
|
52
|
-
process.exit(result.exitCode);
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const scaffoldCommand = defineCommand({
|
|
57
|
-
name: "scaffold",
|
|
58
|
-
description: "Generate doc file from schema template",
|
|
59
|
-
args: rootArgs.extend({
|
|
60
|
-
type: arg(z.enum(ALL_TYPES as unknown as [string, ...string[]]), {
|
|
61
|
-
positional: true,
|
|
62
|
-
description: `Scaffold type (${ALL_TYPES.join(", ")})`,
|
|
63
|
-
}),
|
|
64
|
-
parent: arg(z.string(), {
|
|
65
|
-
positional: true,
|
|
66
|
-
description: "Parent name (module or app name)",
|
|
67
|
-
}),
|
|
68
|
-
name: arg(z.string().optional(), {
|
|
69
|
-
positional: true,
|
|
70
|
-
description: "Item name (required for most types)",
|
|
71
|
-
}),
|
|
72
|
-
}),
|
|
73
|
-
run: async (args) => {
|
|
74
|
-
const paths = requireRoot(args);
|
|
75
|
-
const root = isModuleType(args.type) ? paths.modulesRoot : paths.appRoot;
|
|
76
|
-
if (!root) {
|
|
77
|
-
console.error(
|
|
78
|
-
`--${isModuleType(args.type) ? "modules-root" : "app-root"} is required for scaffold type "${args.type}".`,
|
|
79
|
-
);
|
|
80
|
-
process.exit(2);
|
|
81
|
-
}
|
|
82
|
-
const exitCode = await runScaffold(
|
|
83
|
-
args.type as ScaffoldType,
|
|
84
|
-
args.parent,
|
|
85
|
-
args.name,
|
|
86
|
-
root,
|
|
87
|
-
cwd,
|
|
88
|
-
);
|
|
89
|
-
process.exit(exitCode);
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
12
|
const initCommand = defineCommand({
|
|
94
13
|
name: "init",
|
|
95
14
|
description: "Set up consumer repo with framework skills",
|
|
@@ -107,13 +26,12 @@ const initCommand = defineCommand({
|
|
|
107
26
|
|
|
108
27
|
const main = defineCommand({
|
|
109
28
|
name: "erp-kit",
|
|
110
|
-
description: "
|
|
29
|
+
description: "ERP module framework CLI",
|
|
111
30
|
subCommands: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
scaffold: scaffoldCommand,
|
|
115
|
-
init: initCommand,
|
|
31
|
+
module: moduleCommand,
|
|
32
|
+
app: appCommand,
|
|
116
33
|
mock: mockCommand,
|
|
34
|
+
init: initCommand,
|
|
117
35
|
},
|
|
118
36
|
});
|
|
119
37
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCommand, arg } from "politty";
|
|
3
|
+
import { runCheck } from "../check.js";
|
|
4
|
+
import { runSyncCheck, formatSyncCheckReport } from "../sync-check.js";
|
|
5
|
+
import { runScaffold, APP_TYPES, type ScaffoldType } from "../scaffold.js";
|
|
6
|
+
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
|
|
9
|
+
const rootArgs = z.object({
|
|
10
|
+
root: arg(z.string(), {
|
|
11
|
+
alias: "r",
|
|
12
|
+
description: "Path to app-compose directory",
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const checkCommand = defineCommand({
|
|
17
|
+
name: "check",
|
|
18
|
+
description: "Validate app docs against schemas",
|
|
19
|
+
args: rootArgs,
|
|
20
|
+
run: async (args) => {
|
|
21
|
+
const exitCode = await runCheck({ appRoot: args.root }, cwd);
|
|
22
|
+
process.exit(exitCode);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const syncCheckCommand = defineCommand({
|
|
27
|
+
name: "sync-check",
|
|
28
|
+
description: "Validate source <-> doc correspondence",
|
|
29
|
+
args: rootArgs,
|
|
30
|
+
run: async (args) => {
|
|
31
|
+
const result = await runSyncCheck({ appRoot: args.root }, cwd);
|
|
32
|
+
console.log(formatSyncCheckReport(result));
|
|
33
|
+
process.exit(result.exitCode);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const scaffoldCommand = defineCommand({
|
|
38
|
+
name: "scaffold",
|
|
39
|
+
description: "Generate app doc from schema template",
|
|
40
|
+
args: rootArgs.extend({
|
|
41
|
+
type: arg(z.enum(APP_TYPES as unknown as [string, ...string[]]), {
|
|
42
|
+
positional: true,
|
|
43
|
+
description: `Scaffold type (${APP_TYPES.join(", ")})`,
|
|
44
|
+
}),
|
|
45
|
+
parent: arg(z.string(), {
|
|
46
|
+
positional: true,
|
|
47
|
+
description: "App name",
|
|
48
|
+
}),
|
|
49
|
+
name: arg(z.string().optional(), {
|
|
50
|
+
positional: true,
|
|
51
|
+
description: "Item name (required for most types)",
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
run: async (args) => {
|
|
55
|
+
const exitCode = await runScaffold(
|
|
56
|
+
args.type as ScaffoldType,
|
|
57
|
+
args.parent,
|
|
58
|
+
args.name,
|
|
59
|
+
args.root,
|
|
60
|
+
cwd,
|
|
61
|
+
);
|
|
62
|
+
process.exit(exitCode);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const appCommand = defineCommand({
|
|
67
|
+
name: "app",
|
|
68
|
+
description: "App-compose management",
|
|
69
|
+
subCommands: {
|
|
70
|
+
check: checkCommand,
|
|
71
|
+
"sync-check": syncCheckCommand,
|
|
72
|
+
scaffold: scaffoldCommand,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -8,6 +8,7 @@ describe("buildCheckTargets", () => {
|
|
|
8
8
|
{ glob: "modules/[a-zA-Z]*/docs/features/*.md", schemaKey: "feature" },
|
|
9
9
|
{ glob: "modules/[a-zA-Z]*/docs/commands/*.md", schemaKey: "command" },
|
|
10
10
|
{ glob: "modules/[a-zA-Z]*/docs/models/*.md", schemaKey: "model" },
|
|
11
|
+
{ glob: "modules/[a-zA-Z]*/docs/queries/*.md", schemaKey: "query" },
|
|
11
12
|
{ glob: "modules/[a-zA-Z]*/README.md", schemaKey: "module" },
|
|
12
13
|
]);
|
|
13
14
|
});
|
|
@@ -20,7 +21,7 @@ describe("buildCheckTargets", () => {
|
|
|
20
21
|
|
|
21
22
|
it("generates both when both roots are set", () => {
|
|
22
23
|
const targets = buildCheckTargets({ modulesRoot: "modules", appRoot: "examples" });
|
|
23
|
-
expect(targets).toHaveLength(
|
|
24
|
+
expect(targets).toHaveLength(11);
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
it("returns empty when neither root is set", () => {
|
package/src/commands/check.ts
CHANGED
|
@@ -17,6 +17,7 @@ export function buildCheckTargets(config: {
|
|
|
17
17
|
{ glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
|
|
18
18
|
{ glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
|
|
19
19
|
{ glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
|
|
20
|
+
{ glob: `${m}/[a-zA-Z]*/docs/queries/*.md`, schemaKey: "query" },
|
|
20
21
|
{ glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" },
|
|
21
22
|
);
|
|
22
23
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCommand, arg } from "politty";
|
|
3
|
+
import { runCheck } from "../check.js";
|
|
4
|
+
import { runSyncCheck, formatSyncCheckReport } from "../sync-check.js";
|
|
5
|
+
import { runScaffold, MODULE_TYPES, type ScaffoldType } from "../scaffold.js";
|
|
6
|
+
import { runModuleList } from "./list.js";
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
|
|
10
|
+
const rootArgs = z.object({
|
|
11
|
+
root: arg(z.string(), {
|
|
12
|
+
alias: "r",
|
|
13
|
+
description: "Path to modules directory",
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const listCommand = defineCommand({
|
|
18
|
+
name: "list",
|
|
19
|
+
description: "List available modules",
|
|
20
|
+
run: () => {
|
|
21
|
+
const exitCode = runModuleList();
|
|
22
|
+
process.exit(exitCode);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const checkCommand = defineCommand({
|
|
27
|
+
name: "check",
|
|
28
|
+
description: "Validate module docs against schemas",
|
|
29
|
+
args: rootArgs,
|
|
30
|
+
run: async (args) => {
|
|
31
|
+
const exitCode = await runCheck({ modulesRoot: args.root }, cwd);
|
|
32
|
+
process.exit(exitCode);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const syncCheckCommand = defineCommand({
|
|
37
|
+
name: "sync-check",
|
|
38
|
+
description: "Validate source <-> doc correspondence",
|
|
39
|
+
args: rootArgs,
|
|
40
|
+
run: async (args) => {
|
|
41
|
+
const result = await runSyncCheck({ modulesRoot: args.root }, cwd);
|
|
42
|
+
console.log(formatSyncCheckReport(result));
|
|
43
|
+
process.exit(result.exitCode);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const scaffoldCommand = defineCommand({
|
|
48
|
+
name: "scaffold",
|
|
49
|
+
description: "Generate module doc from schema template",
|
|
50
|
+
args: rootArgs.extend({
|
|
51
|
+
type: arg(z.enum(MODULE_TYPES as unknown as [string, ...string[]]), {
|
|
52
|
+
positional: true,
|
|
53
|
+
description: `Scaffold type (${MODULE_TYPES.join(", ")})`,
|
|
54
|
+
}),
|
|
55
|
+
parent: arg(z.string(), {
|
|
56
|
+
positional: true,
|
|
57
|
+
description: "Module name",
|
|
58
|
+
}),
|
|
59
|
+
name: arg(z.string().optional(), {
|
|
60
|
+
positional: true,
|
|
61
|
+
description: "Item name (required for feature, command, model)",
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
run: async (args) => {
|
|
65
|
+
const exitCode = await runScaffold(
|
|
66
|
+
args.type as ScaffoldType,
|
|
67
|
+
args.parent,
|
|
68
|
+
args.name,
|
|
69
|
+
args.root,
|
|
70
|
+
cwd,
|
|
71
|
+
);
|
|
72
|
+
process.exit(exitCode);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const moduleCommand = defineCommand({
|
|
77
|
+
name: "module",
|
|
78
|
+
description: "Module management",
|
|
79
|
+
subCommands: {
|
|
80
|
+
list: listCommand,
|
|
81
|
+
check: checkCommand,
|
|
82
|
+
"sync-check": syncCheckCommand,
|
|
83
|
+
scaffold: scaffoldCommand,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("listModules", () => {
|
|
4
|
+
it("returns the built-in modules with counts", async () => {
|
|
5
|
+
const { listModules } = await import("./list.js");
|
|
6
|
+
const modules = listModules();
|
|
7
|
+
expect(modules.length).toBeGreaterThan(0);
|
|
8
|
+
|
|
9
|
+
const primitives = modules.find((m) => m.name === "primitives");
|
|
10
|
+
expect(primitives).toBeDefined();
|
|
11
|
+
expect(primitives!.commands).toBeGreaterThan(0);
|
|
12
|
+
expect(primitives!.models).toBeGreaterThan(0);
|
|
13
|
+
expect(primitives!.features).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns zero counts for stub modules", async () => {
|
|
17
|
+
const { listModules } = await import("./list.js");
|
|
18
|
+
const modules = listModules();
|
|
19
|
+
const inventory = modules.find((m) => m.name === "inventory");
|
|
20
|
+
expect(inventory).toBeDefined();
|
|
21
|
+
expect(inventory!.commands).toBe(0);
|
|
22
|
+
expect(inventory!.models).toBe(0);
|
|
23
|
+
expect(inventory!.features).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("excludes shared and testing directories", async () => {
|
|
27
|
+
const { listModules } = await import("./list.js");
|
|
28
|
+
const modules = listModules();
|
|
29
|
+
const names = modules.map((m) => m.name);
|
|
30
|
+
expect(names).not.toContain("shared");
|
|
31
|
+
expect(names).not.toContain("testing");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns modules in alphabetical order", async () => {
|
|
35
|
+
const { listModules } = await import("./list.js");
|
|
36
|
+
const modules = listModules();
|
|
37
|
+
const names = modules.map((m) => m.name);
|
|
38
|
+
const sorted = [...names].sort();
|
|
39
|
+
expect(names).toEqual(sorted);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("formatModuleList", () => {
|
|
44
|
+
it("formats modules with counts", async () => {
|
|
45
|
+
const { formatModuleList } = await import("./list.js");
|
|
46
|
+
const output = formatModuleList([
|
|
47
|
+
{ name: "primitives", commands: 14, models: 4, features: 4 },
|
|
48
|
+
{ name: "inventory", commands: 0, models: 0, features: 0 },
|
|
49
|
+
]);
|
|
50
|
+
expect(output).toContain("primitives");
|
|
51
|
+
expect(output).toContain("14 commands");
|
|
52
|
+
expect(output).toContain("4 models");
|
|
53
|
+
expect(output).toContain("4 features");
|
|
54
|
+
expect(output).toContain("inventory");
|
|
55
|
+
expect(output).toContain("2 modules");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("handles empty list", async () => {
|
|
59
|
+
const { formatModuleList } = await import("./list.js");
|
|
60
|
+
expect(formatModuleList([])).toBe("No modules found.");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { PACKAGE_ROOT } from "../../util.js";
|
|
5
|
+
|
|
6
|
+
const MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
|
|
7
|
+
const EXCLUDED_DIRS = new Set(["shared", "testing"]);
|
|
8
|
+
|
|
9
|
+
export interface ModuleInfo {
|
|
10
|
+
name: string;
|
|
11
|
+
commands: number;
|
|
12
|
+
models: number;
|
|
13
|
+
features: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function countFiles(dir: string, pattern: RegExp, exclusions: RegExp[]): number {
|
|
17
|
+
if (!existsSync(dir)) return 0;
|
|
18
|
+
return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f)))
|
|
19
|
+
.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function listModules(): ModuleInfo[] {
|
|
23
|
+
if (!existsSync(MODULES_DIR)) return [];
|
|
24
|
+
return readdirSync(MODULES_DIR, { withFileTypes: true })
|
|
25
|
+
.filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name))
|
|
26
|
+
.map((d) => {
|
|
27
|
+
const modDir = join(MODULES_DIR, d.name);
|
|
28
|
+
return {
|
|
29
|
+
name: d.name,
|
|
30
|
+
commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
|
|
31
|
+
models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
|
|
32
|
+
features: countFiles(join(modDir, "docs", "features"), /\.md$/, []),
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatModuleList(modules: ModuleInfo[]): string {
|
|
39
|
+
if (modules.length === 0) return "No modules found.";
|
|
40
|
+
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
lines.push(chalk.bold("Modules:\n"));
|
|
43
|
+
|
|
44
|
+
const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
|
|
45
|
+
|
|
46
|
+
for (const mod of modules) {
|
|
47
|
+
const counts = [
|
|
48
|
+
`${mod.commands} commands`,
|
|
49
|
+
`${mod.models} models`,
|
|
50
|
+
`${mod.features} features`,
|
|
51
|
+
].join(", ");
|
|
52
|
+
lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`${modules.length} modules`);
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function runModuleList(): number {
|
|
61
|
+
const modules = listModules();
|
|
62
|
+
console.log(formatModuleList(modules));
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
@@ -61,6 +61,11 @@ describe("resolveScaffoldPath", () => {
|
|
|
61
61
|
expect(result).toBe("examples/my-app/docs/resolver/create-supplier.md");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it("resolves query scaffold path", () => {
|
|
65
|
+
const result = resolveScaffoldPath("query", "inventory", "ConvertQuantity", "modules");
|
|
66
|
+
expect(result).toBe("modules/inventory/docs/queries/ConvertQuantity.md");
|
|
67
|
+
});
|
|
68
|
+
|
|
64
69
|
// Validation
|
|
65
70
|
it("throws when name is required but missing", () => {
|
|
66
71
|
expect(() => resolveScaffoldPath("command", "inventory", undefined, "modules")).toThrow(
|
package/src/commands/scaffold.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import { runMdschema } from "../mdschema.js";
|
|
4
4
|
import { ALL_SCHEMAS } from "../schemas.js";
|
|
5
5
|
|
|
6
|
-
export const MODULE_TYPES = ["module", "feature", "command", "model"] as const;
|
|
6
|
+
export const MODULE_TYPES = ["module", "feature", "command", "model", "query"] as const;
|
|
7
7
|
export const APP_TYPES = [
|
|
8
8
|
"requirements",
|
|
9
9
|
"actors",
|
|
@@ -20,6 +20,7 @@ const MODULE_DIR_MAP: Record<string, string> = {
|
|
|
20
20
|
feature: "docs/features",
|
|
21
21
|
command: "docs/commands",
|
|
22
22
|
model: "docs/models",
|
|
23
|
+
query: "docs/queries",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const APP_DIR_MAP: Record<string, string> = {
|
|
@@ -103,6 +103,34 @@ describe("runSyncCheck", () => {
|
|
|
103
103
|
expect(result.summary.totalSources).toBe(1);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
it("reports missing doc for query source without doc", async () => {
|
|
107
|
+
const srcDir = path.join(tmpDir, "modules", "foo", "query");
|
|
108
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
|
|
110
|
+
|
|
111
|
+
const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
|
|
112
|
+
expect(result.exitCode).toBe(1);
|
|
113
|
+
expect(result.errors).toContainEqual(
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
type: "missing-doc",
|
|
116
|
+
category: "query",
|
|
117
|
+
expectedBasename: "convertamount",
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns 0 when query source and doc match", async () => {
|
|
123
|
+
const srcDir = path.join(tmpDir, "modules", "foo", "query");
|
|
124
|
+
const docDir = path.join(tmpDir, "modules", "foo", "docs", "queries");
|
|
125
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
126
|
+
fs.mkdirSync(docDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
|
|
128
|
+
fs.writeFileSync(path.join(docDir, "convertAmount.md"), "# ConvertAmount");
|
|
129
|
+
|
|
130
|
+
const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
|
|
131
|
+
expect(result.exitCode).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
106
134
|
it("checks only module patterns when appRoot is not set", async () => {
|
|
107
135
|
const resolverSrcDir = path.join(
|
|
108
136
|
tmpDir,
|
|
@@ -41,6 +41,12 @@ function moduleCategories(root: string): CategoryConfig[] {
|
|
|
41
41
|
docPattern: `${root}/*/docs/models/*.md`,
|
|
42
42
|
exclusions: [/\.test\.ts$/, /^index\.ts$/],
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "query",
|
|
46
|
+
sourcePattern: `${root}/*/query/*.ts`,
|
|
47
|
+
docPattern: `${root}/*/docs/queries/*.md`,
|
|
48
|
+
exclusions: [/\.test\.ts$/],
|
|
49
|
+
},
|
|
44
50
|
];
|
|
45
51
|
}
|
|
46
52
|
|
package/src/integration.test.ts
CHANGED
|
@@ -11,13 +11,11 @@ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
|
|
|
11
11
|
const skipReason = fs.existsSync(CLI_PATH) ? undefined : "Run `pnpm build` first";
|
|
12
12
|
|
|
13
13
|
describe.skipIf(skipReason)("integration", () => {
|
|
14
|
-
it("check command runs against sdk-plugins modules", () => {
|
|
15
|
-
// mdschema may report validation errors in real docs, so we allow non-zero exit codes
|
|
16
|
-
// but verify the CLI itself doesn't crash with an unexpected error
|
|
14
|
+
it("module check command runs against sdk-plugins modules", () => {
|
|
17
15
|
try {
|
|
18
16
|
const result = execFileSync(
|
|
19
17
|
"node",
|
|
20
|
-
[CLI_PATH, "check", "--
|
|
18
|
+
[CLI_PATH, "module", "check", "--root", "packages/erp-kit/src/modules"],
|
|
21
19
|
{
|
|
22
20
|
cwd: REPO_ROOT,
|
|
23
21
|
encoding: "utf-8",
|
|
@@ -33,11 +31,11 @@ describe.skipIf(skipReason)("integration", () => {
|
|
|
33
31
|
}
|
|
34
32
|
});
|
|
35
33
|
|
|
36
|
-
it("sync-check command runs against sdk-plugins modules", () => {
|
|
34
|
+
it("module sync-check command runs against sdk-plugins modules", () => {
|
|
37
35
|
try {
|
|
38
36
|
const result = execFileSync(
|
|
39
37
|
"node",
|
|
40
|
-
[CLI_PATH, "sync-check", "--
|
|
38
|
+
[CLI_PATH, "module", "sync-check", "--root", "packages/erp-kit/src/modules"],
|
|
41
39
|
{
|
|
42
40
|
cwd: REPO_ROOT,
|
|
43
41
|
encoding: "utf-8",
|
|
@@ -57,7 +55,7 @@ describe.skipIf(skipReason)("integration", () => {
|
|
|
57
55
|
encoding: "utf-8",
|
|
58
56
|
});
|
|
59
57
|
expect(result).toContain("erp-kit");
|
|
60
|
-
expect(result).toContain("
|
|
61
|
-
expect(result).toContain("
|
|
58
|
+
expect(result).toContain("module");
|
|
59
|
+
expect(result).toContain("app");
|
|
62
60
|
});
|
|
63
61
|
});
|
package/src/module.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// shared/internal
|
|
2
2
|
export { defineCommand, type Command } from "./modules/shared/defineCommand";
|
|
3
|
+
export { defineQuery, type Query } from "./modules/shared/defineQuery";
|
|
3
4
|
export { definePermissions } from "./modules/shared/definePermissions";
|
|
4
5
|
export { requirePermission } from "./modules/shared/requirePermission";
|
|
5
6
|
export { createDomainError, InsufficientPermissionError } from "./modules/shared/errors";
|
|
6
|
-
export type { CommandContext } from "./modules/shared/types";
|
|
7
|
+
export type { CommandContext, QueryContext, ReadonlyDB } from "./modules/shared/types";
|
|
7
8
|
export type {
|
|
8
9
|
InferSchema,
|
|
9
10
|
Selectable,
|
|
@@ -43,13 +44,13 @@ export {
|
|
|
43
44
|
SameCurrencyPairError,
|
|
44
45
|
InvalidExchangeRateError,
|
|
45
46
|
} from "./modules/primitives/lib/errors";
|
|
46
|
-
export { type ConvertQuantityInput } from "./modules/primitives/
|
|
47
|
+
export { type ConvertQuantityInput } from "./modules/primitives/query/convertQuantity";
|
|
47
48
|
export { type ActivateCategoryInput } from "./modules/primitives/command/activateCategory";
|
|
48
49
|
export { type DeactivateCategoryInput } from "./modules/primitives/command/deactivateCategory";
|
|
49
50
|
export { type SetReferenceUnitInput } from "./modules/primitives/command/setReferenceUnit";
|
|
50
51
|
export { type ActivateUnitInput } from "./modules/primitives/command/activateUnit";
|
|
51
52
|
export { type DeactivateUnitInput } from "./modules/primitives/command/deactivateUnit";
|
|
52
|
-
export { type ConvertAmountInput } from "./modules/primitives/
|
|
53
|
+
export { type ConvertAmountInput } from "./modules/primitives/query/convertAmount";
|
|
53
54
|
export { type ActivateCurrencyInput } from "./modules/primitives/command/activateCurrency";
|
|
54
55
|
export { type DeactivateCurrencyInput } from "./modules/primitives/command/deactivateCurrency";
|
|
55
56
|
export { type SetBaseCurrencyInput } from "./modules/primitives/command/setBaseCurrency";
|