@mettlecast/domain-cli 0.2.0
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 +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace all {{TOKEN}} placeholders in a template string with values from ctx.
|
|
3
|
+
* Throws if an unknown token is encountered.
|
|
4
|
+
*/
|
|
5
|
+
export function applyTokens(template, ctx) {
|
|
6
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (match, key) => {
|
|
7
|
+
const value = ctx[key];
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
throw new Error(`Unknown scaffold token: {{${key}}}`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a TokenContext from a scaffold config and resolved version.
|
|
16
|
+
*/
|
|
17
|
+
export function buildTokenContext(config, scaffoldVersion, cliVersion) {
|
|
18
|
+
const name = config.projectName;
|
|
19
|
+
return {
|
|
20
|
+
PROJECT_NAME: name,
|
|
21
|
+
PROJECT_NAME_PASCAL: toPascalCase(name),
|
|
22
|
+
PROJECT_NAME_UPPER: toUpperSnakeCase(name),
|
|
23
|
+
AWS_REGION: config.awsRegion,
|
|
24
|
+
AWS_ACCOUNT_ID: config.awsAccountId,
|
|
25
|
+
ADMIN_EMAIL: config.adminEmail ?? '',
|
|
26
|
+
SCAFFOLD_VERSION: scaffoldVersion,
|
|
27
|
+
SCAFFOLD_DATE: new Date().toISOString().split('T')[0],
|
|
28
|
+
TIB_CLI_VERSION: cliVersion,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function toPascalCase(kebab) {
|
|
32
|
+
return kebab
|
|
33
|
+
.split('-')
|
|
34
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
35
|
+
.join('');
|
|
36
|
+
}
|
|
37
|
+
function toUpperSnakeCase(kebab) {
|
|
38
|
+
return kebab.replace(/-/g, '_').toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the given file extension indicates the file should receive token substitution.
|
|
42
|
+
* Only .tpl files are substituted.
|
|
43
|
+
*/
|
|
44
|
+
export function isTemplateFile(filename) {
|
|
45
|
+
return filename.endsWith('.tpl');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Strip the .tpl extension from a filename to get the install path.
|
|
49
|
+
*/
|
|
50
|
+
export function stripTplExtension(filename) {
|
|
51
|
+
return filename.endsWith('.tpl') ? filename.slice(0, -4) : filename;
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for extracting Zod schemas to JSON Schema format.
|
|
3
|
+
* Uses zod-to-json-schema to convert live Zod objects to JSON-serializable schema.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract a Zod schema to its JSON Schema representation.
|
|
7
|
+
* @param schema - A Zod schema object (e.g., z.object({...})).
|
|
8
|
+
* @returns JSON Schema object or null if extraction fails.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractZodSchema(schema: unknown): Record<string, unknown> | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for extracting Zod schemas to JSON Schema format.
|
|
3
|
+
* Uses zod-to-json-schema to convert live Zod objects to JSON-serializable schema.
|
|
4
|
+
*/
|
|
5
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
+
/**
|
|
7
|
+
* Extract a Zod schema to its JSON Schema representation.
|
|
8
|
+
* @param schema - A Zod schema object (e.g., z.object({...})).
|
|
9
|
+
* @returns JSON Schema object or null if extraction fails.
|
|
10
|
+
*/
|
|
11
|
+
export function extractZodSchema(schema) {
|
|
12
|
+
if (!schema || typeof schema !== 'object') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
// Check if this looks like a Zod schema by inspecting for the parse method
|
|
17
|
+
if (typeof schema.parse === 'function') {
|
|
18
|
+
// Use type assertion to bypass TypeScript compatibility issues with zod versions
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const jsonSchema = zodToJsonSchema(schema);
|
|
21
|
+
return jsonSchema;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Silently return null if extraction fails
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mettlecast/domain-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"mc-domain-module": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
18
|
+
"dev": "tsx ./src/cli.ts",
|
|
19
|
+
"test": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
23
|
+
"@aws-sdk/client-sfn": "^3.0.0",
|
|
24
|
+
"@mettlecast/domain-runtime": "*",
|
|
25
|
+
"@mettlecast/domain-cdk-packer": "*",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"fastify": "^5.0.0",
|
|
28
|
+
"tar": "^7.0.0",
|
|
29
|
+
"tsx": "^4.0.0",
|
|
30
|
+
"zod-to-json-schema": "^3.0.0",
|
|
31
|
+
"pino": "^9.0.0",
|
|
32
|
+
"pino-pretty": "^11.0.0",
|
|
33
|
+
"dotenv": "^16.0.0",
|
|
34
|
+
"semver": "^7.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"@types/semver": "^7.0.0",
|
|
39
|
+
"@types/tar": "^6.0.0",
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"vitest": "^2.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../builder/load-module.js', () => ({
|
|
4
|
+
loadModuleExports: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock('../utils/file-helpers.js', () => ({
|
|
7
|
+
walkDomainDir: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { buildRegistry } from '../builder/build-registry.js';
|
|
11
|
+
import { loadModuleExports } from '../builder/load-module.js';
|
|
12
|
+
import { walkDomainDir } from '../utils/file-helpers.js';
|
|
13
|
+
|
|
14
|
+
const mockLoadModuleExports = vi.mocked(loadModuleExports);
|
|
15
|
+
const mockWalkDomainDir = vi.mocked(walkDomainDir);
|
|
16
|
+
|
|
17
|
+
describe('buildRegistry', () => {
|
|
18
|
+
const DOMAIN_ROOT = '/fake/domains/billing';
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('throws if domain.config.ts is missing', async () => {
|
|
25
|
+
mockWalkDomainDir.mockResolvedValue({
|
|
26
|
+
domain: undefined,
|
|
27
|
+
apis: [],
|
|
28
|
+
webhooks: [],
|
|
29
|
+
subscribers: [],
|
|
30
|
+
actions: [],
|
|
31
|
+
schedules: [],
|
|
32
|
+
jobs: [],
|
|
33
|
+
integrations: [],
|
|
34
|
+
publishes: undefined,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await expect(buildRegistry(DOMAIN_ROOT)).rejects.toThrow('domain.config.ts not found');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('throws if domain.config.ts has no domain export', async () => {
|
|
41
|
+
mockWalkDomainDir.mockResolvedValue({
|
|
42
|
+
domain: '/fake/domains/billing/domain.config.ts',
|
|
43
|
+
apis: [],
|
|
44
|
+
webhooks: [],
|
|
45
|
+
subscribers: [],
|
|
46
|
+
actions: [],
|
|
47
|
+
schedules: [],
|
|
48
|
+
jobs: [],
|
|
49
|
+
integrations: [],
|
|
50
|
+
publishes: undefined,
|
|
51
|
+
});
|
|
52
|
+
mockLoadModuleExports.mockResolvedValue([]);
|
|
53
|
+
|
|
54
|
+
await expect(buildRegistry(DOMAIN_ROOT)).rejects.toThrow("no 'domain' export found");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('builds a registry with domain + one api', async () => {
|
|
58
|
+
mockWalkDomainDir.mockResolvedValue({
|
|
59
|
+
domain: '/fake/domains/billing/domain.config.ts',
|
|
60
|
+
apis: ['/fake/domains/billing/api/get-invoice.ts'],
|
|
61
|
+
webhooks: [],
|
|
62
|
+
subscribers: [],
|
|
63
|
+
actions: [],
|
|
64
|
+
schedules: [],
|
|
65
|
+
jobs: [],
|
|
66
|
+
integrations: [],
|
|
67
|
+
publishes: undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
mockLoadModuleExports
|
|
71
|
+
.mockResolvedValueOnce([
|
|
72
|
+
{ _kind: 'domain', _exportName: 'default', id: 'billing', name: 'Billing', tenancy: 'required' },
|
|
73
|
+
])
|
|
74
|
+
.mockResolvedValueOnce([
|
|
75
|
+
{ _kind: 'api', _exportName: 'getInvoice', id: 'get-invoice', path: '/invoices/:id', auth: { type: 'jwt' } },
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const { registry, warnings } = await buildRegistry(DOMAIN_ROOT);
|
|
79
|
+
|
|
80
|
+
expect(warnings).toHaveLength(0);
|
|
81
|
+
expect(registry.schemaVersion).toBe('1');
|
|
82
|
+
expect(registry.domain.id).toBe('billing');
|
|
83
|
+
expect(registry.apis).toHaveLength(1);
|
|
84
|
+
expect(registry.apis[0]!.id).toBe('get-invoice');
|
|
85
|
+
expect(registry.apis[0]!.path).toBe('/invoices/:id');
|
|
86
|
+
expect(registry.apis[0]!.authType).toBe('jwt');
|
|
87
|
+
expect(registry.apis[0]!.handlerFile).toBe('api/get-invoice.ts');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('collects warnings when a file fails to load', async () => {
|
|
91
|
+
mockWalkDomainDir.mockResolvedValue({
|
|
92
|
+
domain: '/fake/domains/billing/domain.config.ts',
|
|
93
|
+
apis: ['/fake/domains/billing/api/broken.ts'],
|
|
94
|
+
webhooks: [],
|
|
95
|
+
subscribers: [],
|
|
96
|
+
actions: [],
|
|
97
|
+
schedules: [],
|
|
98
|
+
jobs: [],
|
|
99
|
+
integrations: [],
|
|
100
|
+
publishes: undefined,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
mockLoadModuleExports
|
|
104
|
+
.mockResolvedValueOnce([
|
|
105
|
+
{ _kind: 'domain', _exportName: 'default', id: 'billing', name: 'Billing', tenancy: 'required' },
|
|
106
|
+
])
|
|
107
|
+
.mockRejectedValueOnce(new Error('tsx error'));
|
|
108
|
+
|
|
109
|
+
const { registry, warnings } = await buildRegistry(DOMAIN_ROOT);
|
|
110
|
+
|
|
111
|
+
expect(warnings).toHaveLength(1);
|
|
112
|
+
expect(warnings[0]).toContain('tsx error');
|
|
113
|
+
expect(registry.apis).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildActionsTypes } from '../builder/build-types.js';
|
|
3
|
+
import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
|
|
4
|
+
|
|
5
|
+
describe('buildActionsTypes', () => {
|
|
6
|
+
it('generates .d.ts with action overloads', () => {
|
|
7
|
+
const registry: DomainRegistry = {
|
|
8
|
+
schemaVersion: '1',
|
|
9
|
+
domainRoot: '/test/auth',
|
|
10
|
+
domain: { id: 'auth', kind: 'domain', name: 'Auth', tenancy: 'required' },
|
|
11
|
+
apis: [],
|
|
12
|
+
webhooks: [],
|
|
13
|
+
subscribers: [],
|
|
14
|
+
schedules: [],
|
|
15
|
+
jobs: [],
|
|
16
|
+
actions: [
|
|
17
|
+
{
|
|
18
|
+
id: 'verify-token',
|
|
19
|
+
kind: 'action',
|
|
20
|
+
handlerFile: 'verify-token.ts',
|
|
21
|
+
visibility: 'domain',
|
|
22
|
+
idempotent: false,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
integrations: [],
|
|
26
|
+
events: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const output = buildActionsTypes([registry]);
|
|
30
|
+
|
|
31
|
+
expect(output).toContain("declare module '@mettlecast/domain-runtime'");
|
|
32
|
+
expect(output).toContain("interface ActionsProxy");
|
|
33
|
+
expect(output).toContain("call(actionId: 'auth.verify-token'");
|
|
34
|
+
expect(output).toContain("Promise<unknown>");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('generates overloads for multiple domains', () => {
|
|
38
|
+
const auth: DomainRegistry = {
|
|
39
|
+
schemaVersion: '1',
|
|
40
|
+
domainRoot: '/test/auth',
|
|
41
|
+
domain: { id: 'auth', kind: 'domain', name: 'Auth', tenancy: 'required' },
|
|
42
|
+
apis: [],
|
|
43
|
+
webhooks: [],
|
|
44
|
+
subscribers: [],
|
|
45
|
+
schedules: [],
|
|
46
|
+
jobs: [],
|
|
47
|
+
actions: [
|
|
48
|
+
{ id: 'verify', kind: 'action', handlerFile: 'verify.ts', visibility: 'workspace', idempotent: true },
|
|
49
|
+
],
|
|
50
|
+
integrations: [],
|
|
51
|
+
events: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const payments: DomainRegistry = {
|
|
55
|
+
schemaVersion: '1',
|
|
56
|
+
domainRoot: '/test/payments',
|
|
57
|
+
domain: { id: 'payments', kind: 'domain', name: 'Payments', tenancy: 'required' },
|
|
58
|
+
apis: [],
|
|
59
|
+
webhooks: [],
|
|
60
|
+
subscribers: [],
|
|
61
|
+
schedules: [],
|
|
62
|
+
jobs: [],
|
|
63
|
+
actions: [
|
|
64
|
+
{ id: 'charge', kind: 'action', handlerFile: 'charge.ts', visibility: 'domain', idempotent: false },
|
|
65
|
+
],
|
|
66
|
+
integrations: [],
|
|
67
|
+
events: [],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const output = buildActionsTypes([auth, payments]);
|
|
71
|
+
|
|
72
|
+
expect(output).toContain("call(actionId: 'auth.verify'");
|
|
73
|
+
expect(output).toContain("call(actionId: 'payments.charge'");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns valid TypeScript .d.ts syntax', () => {
|
|
77
|
+
const registry: DomainRegistry = {
|
|
78
|
+
schemaVersion: '1',
|
|
79
|
+
domainRoot: '/test',
|
|
80
|
+
domain: { id: 'test', kind: 'domain', name: 'Test', tenancy: 'none' },
|
|
81
|
+
apis: [],
|
|
82
|
+
webhooks: [],
|
|
83
|
+
subscribers: [],
|
|
84
|
+
schedules: [],
|
|
85
|
+
jobs: [],
|
|
86
|
+
actions: [],
|
|
87
|
+
integrations: [],
|
|
88
|
+
events: [],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const output = buildActionsTypes([registry]);
|
|
92
|
+
|
|
93
|
+
// Should start with comment
|
|
94
|
+
expect(output).toMatch(/^\/\/ Generated by/);
|
|
95
|
+
// Should have export
|
|
96
|
+
expect(output).toContain('export {};');
|
|
97
|
+
// Should close module
|
|
98
|
+
expect(output).toContain('}');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, rmdir, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { walkDomainDir } from '../../utils/file-helpers.js';
|
|
6
|
+
|
|
7
|
+
describe('walkDomainDir recursive walk', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'tib-walk-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
try {
|
|
16
|
+
await rmdir(tmpDir, { recursive: true });
|
|
17
|
+
} catch {
|
|
18
|
+
// Ignore cleanup errors
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should find .ts files at root of api directory', async () => {
|
|
23
|
+
const apiDir = join(tmpDir, 'api');
|
|
24
|
+
await mkdir(apiDir, { recursive: true });
|
|
25
|
+
await writeFile(join(apiDir, 'simple.ts'), 'export const api = {}');
|
|
26
|
+
|
|
27
|
+
const paths = await walkDomainDir(tmpDir);
|
|
28
|
+
|
|
29
|
+
expect(paths.apis).toContain(join(apiDir, 'simple.ts'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should recursively find .ts files in nested api directories', async () => {
|
|
33
|
+
const nestedDir = join(tmpDir, 'api', 'users', 'v1');
|
|
34
|
+
await mkdir(nestedDir, { recursive: true });
|
|
35
|
+
await writeFile(join(nestedDir, 'get.ts'), 'export const api = {}');
|
|
36
|
+
|
|
37
|
+
const paths = await walkDomainDir(tmpDir);
|
|
38
|
+
|
|
39
|
+
expect(paths.apis).toContain(join(nestedDir, 'get.ts'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should skip __tests__ directories', async () => {
|
|
43
|
+
const testDir = join(tmpDir, 'api', '__tests__');
|
|
44
|
+
await mkdir(testDir, { recursive: true });
|
|
45
|
+
await writeFile(join(testDir, 'test.ts'), 'export const test = {}');
|
|
46
|
+
|
|
47
|
+
const paths = await walkDomainDir(tmpDir);
|
|
48
|
+
|
|
49
|
+
expect(paths.apis).not.toContain(join(testDir, 'test.ts'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should skip dist directories', async () => {
|
|
53
|
+
const distDir = join(tmpDir, 'api', 'dist');
|
|
54
|
+
await mkdir(distDir, { recursive: true });
|
|
55
|
+
await writeFile(join(distDir, 'bundle.ts'), 'export const bundle = {}');
|
|
56
|
+
|
|
57
|
+
const paths = await walkDomainDir(tmpDir);
|
|
58
|
+
|
|
59
|
+
expect(paths.apis).not.toContain(join(distDir, 'bundle.ts'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should skip node_modules directories', async () => {
|
|
63
|
+
const nodeModulesDir = join(tmpDir, 'api', 'node_modules');
|
|
64
|
+
await mkdir(nodeModulesDir, { recursive: true });
|
|
65
|
+
await writeFile(join(nodeModulesDir, 'dep.ts'), 'export const dep = {}');
|
|
66
|
+
|
|
67
|
+
const paths = await walkDomainDir(tmpDir);
|
|
68
|
+
|
|
69
|
+
expect(paths.apis).not.toContain(join(nodeModulesDir, 'dep.ts'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should discover files in all domain primitive directories', async () => {
|
|
73
|
+
// Create files in all conventional directories
|
|
74
|
+
const dirs = ['api', 'webhooks', 'subscribers', 'actions', 'schedules', 'jobs', 'integrations'];
|
|
75
|
+
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
const dirPath = join(tmpDir, dir, 'nested', 'deep');
|
|
78
|
+
await mkdir(dirPath, { recursive: true });
|
|
79
|
+
await writeFile(join(dirPath, `${dir}-handler.ts`), `export const ${dir} = {}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const paths = await walkDomainDir(tmpDir);
|
|
83
|
+
|
|
84
|
+
// Verify files are found in correct slots
|
|
85
|
+
expect(paths.apis.some(p => p.includes('api-handler.ts'))).toBe(true);
|
|
86
|
+
expect(paths.webhooks.some(p => p.includes('webhooks-handler.ts'))).toBe(true);
|
|
87
|
+
expect(paths.subscribers.some(p => p.includes('subscribers-handler.ts'))).toBe(true);
|
|
88
|
+
expect(paths.actions.some(p => p.includes('actions-handler.ts'))).toBe(true);
|
|
89
|
+
expect(paths.schedules.some(p => p.includes('schedules-handler.ts'))).toBe(true);
|
|
90
|
+
expect(paths.jobs.some(p => p.includes('jobs-handler.ts'))).toBe(true);
|
|
91
|
+
expect(paths.integrations.some(p => p.includes('integrations-handler.ts'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should find domain.config.ts at root', async () => {
|
|
95
|
+
await writeFile(join(tmpDir, 'domain.config.ts'), 'export const domain = {}');
|
|
96
|
+
|
|
97
|
+
const paths = await walkDomainDir(tmpDir);
|
|
98
|
+
|
|
99
|
+
expect(paths.domain).toBe(join(tmpDir, 'domain.config.ts'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should find publishes/events.ts', async () => {
|
|
103
|
+
const publishDir = join(tmpDir, 'publishes');
|
|
104
|
+
await mkdir(publishDir, { recursive: true });
|
|
105
|
+
await writeFile(join(publishDir, 'events.ts'), 'export const events = {}');
|
|
106
|
+
|
|
107
|
+
const paths = await walkDomainDir(tmpDir);
|
|
108
|
+
|
|
109
|
+
expect(paths.publishes).toBe(join(publishDir, 'events.ts'));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return empty arrays for missing directories', async () => {
|
|
113
|
+
const paths = await walkDomainDir(tmpDir);
|
|
114
|
+
|
|
115
|
+
expect(paths.apis).toEqual([]);
|
|
116
|
+
expect(paths.webhooks).toEqual([]);
|
|
117
|
+
expect(paths.subscribers).toEqual([]);
|
|
118
|
+
expect(paths.actions).toEqual([]);
|
|
119
|
+
expect(paths.schedules).toEqual([]);
|
|
120
|
+
expect(paths.jobs).toEqual([]);
|
|
121
|
+
expect(paths.integrations).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return undefined for missing domain.config.ts and publishes/events.ts', async () => {
|
|
125
|
+
const paths = await walkDomainDir(tmpDir);
|
|
126
|
+
|
|
127
|
+
expect(paths.domain).toBeUndefined();
|
|
128
|
+
expect(paths.publishes).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle deeply nested files', async () => {
|
|
132
|
+
const deepPath = join(tmpDir, 'api', 'v1', 'users', 'admin', 'permissions');
|
|
133
|
+
await mkdir(deepPath, { recursive: true });
|
|
134
|
+
await writeFile(join(deepPath, 'check.ts'), 'export const api = {}');
|
|
135
|
+
|
|
136
|
+
const paths = await walkDomainDir(tmpDir);
|
|
137
|
+
|
|
138
|
+
expect(paths.apis.some(p => p.includes('check.ts'))).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, rmdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
6
|
+
import { runAddDomain, type AddDomainOptions } from '../../commands/add-domain.js';
|
|
7
|
+
import { getScaffoldConfig } from '../../utils/scaffold-config.js';
|
|
8
|
+
|
|
9
|
+
describe('add-domain command', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'tib-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
// Clean up temp directory
|
|
18
|
+
try {
|
|
19
|
+
await rmdir(tmpDir, { recursive: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore cleanup errors
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should scaffold a new domain with valid kebab-case id', async () => {
|
|
26
|
+
const opts: AddDomainOptions = {
|
|
27
|
+
id: 'test-domain',
|
|
28
|
+
tenancy: 'required',
|
|
29
|
+
domainsDir: tmpDir,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await runAddDomain(opts);
|
|
33
|
+
|
|
34
|
+
// Verify directory structure
|
|
35
|
+
const domainDir = join(tmpDir, 'test-domain');
|
|
36
|
+
await stat(join(domainDir, 'api'));
|
|
37
|
+
await stat(join(domainDir, 'subscribers'));
|
|
38
|
+
await stat(join(domainDir, 'publishes'));
|
|
39
|
+
|
|
40
|
+
// Verify skeleton files
|
|
41
|
+
const configContent = await readFile(join(domainDir, 'domain.config.ts'), 'utf8');
|
|
42
|
+
expect(configContent).toContain(`id: 'test-domain'`);
|
|
43
|
+
expect(configContent).toContain(`tenancy: 'required'`);
|
|
44
|
+
|
|
45
|
+
const apiContent = await readFile(join(domainDir, 'api', 'example.ts'), 'utf8');
|
|
46
|
+
expect(apiContent).toContain(`id: 'example'`);
|
|
47
|
+
expect(apiContent).toContain('defineApi');
|
|
48
|
+
|
|
49
|
+
const eventsContent = await readFile(join(domainDir, 'publishes', 'events.ts'), 'utf8');
|
|
50
|
+
expect(eventsContent).toContain(`id: 'test-domain.example'`);
|
|
51
|
+
expect(eventsContent).toContain('defineEvent');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should scaffold CLAUDE.md and README.md with correct content', async () => {
|
|
55
|
+
const opts: AddDomainOptions = {
|
|
56
|
+
id: 'my-feature',
|
|
57
|
+
tenancy: 'required',
|
|
58
|
+
domainsDir: tmpDir,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await runAddDomain(opts);
|
|
62
|
+
|
|
63
|
+
const domainDir = join(tmpDir, 'my-feature');
|
|
64
|
+
|
|
65
|
+
// CLAUDE.md must exist
|
|
66
|
+
await stat(join(domainDir, 'CLAUDE.md'));
|
|
67
|
+
const claudeContent = await readFile(join(domainDir, 'CLAUDE.md'), 'utf8');
|
|
68
|
+
expect(claudeContent).toContain('my-feature');
|
|
69
|
+
expect(claudeContent).toContain('domain owner');
|
|
70
|
+
|
|
71
|
+
// README.md must exist
|
|
72
|
+
await stat(join(domainDir, 'README.md'));
|
|
73
|
+
const readmeContent = await readFile(join(domainDir, 'README.md'), 'utf8');
|
|
74
|
+
// Title-cased domain name should appear in README
|
|
75
|
+
expect(readmeContent).toContain('My Feature');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should register domain in scaffold config', async () => {
|
|
79
|
+
const opts: AddDomainOptions = {
|
|
80
|
+
id: 'another-domain',
|
|
81
|
+
tenancy: 'none',
|
|
82
|
+
domainsDir: tmpDir,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await runAddDomain(opts);
|
|
86
|
+
|
|
87
|
+
// Verify scaffold config
|
|
88
|
+
const config = await getScaffoldConfig(tmpDir);
|
|
89
|
+
expect(config.domainIds).toContain('another-domain');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should be idempotent — calling twice should not duplicate', async () => {
|
|
93
|
+
const opts: AddDomainOptions = {
|
|
94
|
+
id: 'idempotent-domain',
|
|
95
|
+
tenancy: 'system',
|
|
96
|
+
domainsDir: tmpDir,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await runAddDomain(opts);
|
|
100
|
+
|
|
101
|
+
// Read config after first scaffold
|
|
102
|
+
let config = await getScaffoldConfig(tmpDir);
|
|
103
|
+
const count1 = config.domainIds.filter(id => id === 'idempotent-domain').length;
|
|
104
|
+
expect(count1).toBe(1);
|
|
105
|
+
|
|
106
|
+
// Try to scaffold again (should fail because directory exists)
|
|
107
|
+
await expect(runAddDomain(opts)).rejects.toThrow(/already exists/);
|
|
108
|
+
|
|
109
|
+
// Verify config still has only one entry
|
|
110
|
+
config = await getScaffoldConfig(tmpDir);
|
|
111
|
+
const count2 = config.domainIds.filter(id => id === 'idempotent-domain').length;
|
|
112
|
+
expect(count2).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should reject invalid kebab-case ids', async () => {
|
|
116
|
+
const invalidIds = ['MyDomain', '123-foo', 'foo_bar', 'Foo-Bar', '-foo'];
|
|
117
|
+
|
|
118
|
+
for (const id of invalidIds) {
|
|
119
|
+
const opts: AddDomainOptions = {
|
|
120
|
+
id,
|
|
121
|
+
tenancy: 'required',
|
|
122
|
+
domainsDir: tmpDir,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await expect(runAddDomain(opts)).rejects.toThrow(/Invalid domain id/);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should sort domain ids in scaffold config', async () => {
|
|
130
|
+
// Scaffold in non-alphabetical order
|
|
131
|
+
await runAddDomain({
|
|
132
|
+
id: 'zebra-domain',
|
|
133
|
+
tenancy: 'required',
|
|
134
|
+
domainsDir: tmpDir,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await runAddDomain({
|
|
138
|
+
id: 'alpha-domain',
|
|
139
|
+
tenancy: 'required',
|
|
140
|
+
domainsDir: tmpDir,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const config = await getScaffoldConfig(tmpDir);
|
|
144
|
+
expect(config.domainIds).toEqual(['alpha-domain', 'zebra-domain']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should set correct tenancy mode in templates', async () => {
|
|
148
|
+
await runAddDomain({
|
|
149
|
+
id: 'system-domain',
|
|
150
|
+
tenancy: 'system',
|
|
151
|
+
domainsDir: tmpDir,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const configContent = await readFile(join(tmpDir, 'system-domain', 'domain.config.ts'), 'utf8');
|
|
155
|
+
expect(configContent).toContain(`tenancy: 'system'`);
|
|
156
|
+
|
|
157
|
+
const apiContent = await readFile(join(tmpDir, 'system-domain', 'api', 'example.ts'), 'utf8');
|
|
158
|
+
expect(apiContent).toContain(`tenancy: 'system'`);
|
|
159
|
+
// For 'system' tenancy, path should not include /v1/tenants/{tenantId}
|
|
160
|
+
expect(apiContent).toContain(`path: '/v1/system-domain/example'`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should use correct path structure for required tenancy', async () => {
|
|
164
|
+
await runAddDomain({
|
|
165
|
+
id: 'tenant-domain',
|
|
166
|
+
tenancy: 'required',
|
|
167
|
+
domainsDir: tmpDir,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const apiContent = await readFile(join(tmpDir, 'tenant-domain', 'api', 'example.ts'), 'utf8');
|
|
171
|
+
// For 'required' tenancy, path should include /v1/tenants/{tenantId}
|
|
172
|
+
expect(apiContent).toContain(`path: '/v1/tenants/{tenantId}/tenant-domain/example'`);
|
|
173
|
+
});
|
|
174
|
+
});
|