@mettlecast/domain-cli 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/build-registry.js +6 -1
- package/dist/builder/load-module.js +6 -3
- package/dist/commands/build-catalog.d.ts +26 -0
- package/dist/commands/build-catalog.js +37 -3
- package/dist/commands/build.js +3 -2
- package/dist/commands/validate.js +39 -0
- package/package.json +8 -8
- package/src/builder/build-registry.ts +7 -1
- package/src/builder/load-module.ts +6 -3
- package/src/commands/build-catalog.ts +69 -3
- package/src/commands/build.ts +3 -2
- package/src/commands/validate.ts +37 -0
|
@@ -64,9 +64,14 @@ export async function buildRegistry(domainRoot) {
|
|
|
64
64
|
Promise.all(paths.integrations.map(load)),
|
|
65
65
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
66
66
|
]);
|
|
67
|
+
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
67
68
|
const apis = paths.apis.flatMap((filePath, i) => (apiExports[i] ?? [])
|
|
68
69
|
.filter(e => e['_kind'] === 'api')
|
|
69
70
|
.map(e => {
|
|
71
|
+
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
72
|
+
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
73
|
+
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
74
|
+
}
|
|
70
75
|
const rawVersions = e['versions'];
|
|
71
76
|
const versionSnapshots = rawVersions
|
|
72
77
|
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
@@ -81,7 +86,7 @@ export async function buildRegistry(domainRoot) {
|
|
|
81
86
|
kind: 'api',
|
|
82
87
|
handlerFile: relPath(filePath),
|
|
83
88
|
path: String(e['path']),
|
|
84
|
-
method:
|
|
89
|
+
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
85
90
|
authType: e['auth']?.type ?? 'jwt',
|
|
86
91
|
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
87
92
|
deployment: deployment(e),
|
|
@@ -26,20 +26,23 @@ function makeEvalScript(absoluteFilePath) {
|
|
|
26
26
|
// Convert to file:// URL so ESM loader works on all platforms (especially Windows).
|
|
27
27
|
const fileUrl = pathToFileURL(absoluteFilePath).href;
|
|
28
28
|
return `
|
|
29
|
-
import {
|
|
29
|
+
import { z } from 'zod';
|
|
30
30
|
import * as mod from ${JSON.stringify(fileUrl)};
|
|
31
31
|
const KINDS = new Set(['api','webhook','subscriber','schedule','job','action','integration','event','domain']);
|
|
32
32
|
function isZod(v) {
|
|
33
33
|
return v && typeof v === 'object' && typeof v.parse === 'function' && typeof v.safeParse === 'function' && v._def !== undefined;
|
|
34
34
|
}
|
|
35
|
+
function toJsonSchema(v) {
|
|
36
|
+
try { return z.toJSONSchema(v); } catch { /* skip unserializable */ }
|
|
37
|
+
}
|
|
35
38
|
function strip(obj) {
|
|
36
39
|
const out = {};
|
|
37
40
|
for (const [k, v] of Object.entries(obj)) {
|
|
38
41
|
if (typeof v === 'function') continue;
|
|
39
42
|
if (isZod(v)) {
|
|
40
|
-
|
|
43
|
+
out[k] = toJsonSchema(v);
|
|
41
44
|
} else if (Array.isArray(v)) {
|
|
42
|
-
out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? (
|
|
45
|
+
out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? toJsonSchema(item) : item));
|
|
43
46
|
} else if (v && typeof v === 'object') {
|
|
44
47
|
out[k] = strip(v);
|
|
45
48
|
} else {
|
|
@@ -42,6 +42,29 @@ export interface CatalogSubscriber {
|
|
|
42
42
|
semverRange: string;
|
|
43
43
|
description?: string;
|
|
44
44
|
}
|
|
45
|
+
/** A single job entry in the catalog. */
|
|
46
|
+
export interface CatalogJob {
|
|
47
|
+
id: string;
|
|
48
|
+
domainId: string;
|
|
49
|
+
maxRetries: number;
|
|
50
|
+
visibilityTimeoutSeconds: number;
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
/** A single schedule entry in the catalog. */
|
|
54
|
+
export interface CatalogSchedule {
|
|
55
|
+
id: string;
|
|
56
|
+
domainId: string;
|
|
57
|
+
cron: string;
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
description?: string;
|
|
60
|
+
}
|
|
61
|
+
/** A single integration entry in the catalog. */
|
|
62
|
+
export interface CatalogIntegration {
|
|
63
|
+
id: string;
|
|
64
|
+
domainId: string;
|
|
65
|
+
baseUrl: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}
|
|
45
68
|
/** A single domain entry in the catalog. */
|
|
46
69
|
export interface CatalogDomain {
|
|
47
70
|
id: string;
|
|
@@ -58,6 +81,9 @@ export interface DomainCatalog {
|
|
|
58
81
|
actions: CatalogAction[];
|
|
59
82
|
events: CatalogEvent[];
|
|
60
83
|
subscribers: CatalogSubscriber[];
|
|
84
|
+
jobs: CatalogJob[];
|
|
85
|
+
schedules: CatalogSchedule[];
|
|
86
|
+
integrations: CatalogIntegration[];
|
|
61
87
|
}
|
|
62
88
|
/**
|
|
63
89
|
* Build the combined domain catalog from all per-domain registry files.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { cliLogger } from '../utils/logger.js';
|
|
4
4
|
/**
|
|
@@ -29,6 +29,9 @@ export async function runBuildCatalog(tibDir) {
|
|
|
29
29
|
actions: [],
|
|
30
30
|
events: [],
|
|
31
31
|
subscribers: [],
|
|
32
|
+
jobs: [],
|
|
33
|
+
schedules: [],
|
|
34
|
+
integrations: [],
|
|
32
35
|
};
|
|
33
36
|
for (const file of registryFiles) {
|
|
34
37
|
const raw = await readFile(join(dir, file), 'utf8');
|
|
@@ -97,9 +100,40 @@ export async function runBuildCatalog(tibDir) {
|
|
|
97
100
|
description: sub['description'],
|
|
98
101
|
});
|
|
99
102
|
}
|
|
103
|
+
const jobs = registry['jobs'] ?? [];
|
|
104
|
+
for (const job of jobs) {
|
|
105
|
+
catalog.jobs.push({
|
|
106
|
+
id: String(job['id']),
|
|
107
|
+
domainId,
|
|
108
|
+
maxRetries: Number(job['maxRetries'] ?? 3),
|
|
109
|
+
visibilityTimeoutSeconds: Number(job['visibilityTimeoutSeconds'] ?? 300),
|
|
110
|
+
description: job['description'],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const schedules = registry['schedules'] ?? [];
|
|
114
|
+
for (const schedule of schedules) {
|
|
115
|
+
catalog.schedules.push({
|
|
116
|
+
id: String(schedule['id']),
|
|
117
|
+
domainId,
|
|
118
|
+
cron: String(schedule['cron'] ?? ''),
|
|
119
|
+
enabled: Boolean(schedule['enabled'] ?? true),
|
|
120
|
+
description: schedule['description'],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const integrations = registry['integrations'] ?? [];
|
|
124
|
+
for (const integration of integrations) {
|
|
125
|
+
catalog.integrations.push({
|
|
126
|
+
id: String(integration['id']),
|
|
127
|
+
domainId,
|
|
128
|
+
baseUrl: String(integration['baseUrl'] ?? ''),
|
|
129
|
+
description: integration['description'],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
100
132
|
}
|
|
101
|
-
const
|
|
133
|
+
const mcDir = join(process.cwd(), '.mc');
|
|
134
|
+
await mkdir(mcDir, { recursive: true });
|
|
135
|
+
const outPath = join(mcDir, 'domain-registry.json');
|
|
102
136
|
await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
|
|
103
|
-
cliLogger.info({ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length }, 'Domain catalog written');
|
|
137
|
+
cliLogger.info({ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length }, 'Domain catalog written');
|
|
104
138
|
return catalog;
|
|
105
139
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
2
|
+
import { join, resolve, basename } from 'node:path';
|
|
3
3
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
4
4
|
import { buildActionsTypes } from '../builder/build-types.js';
|
|
5
5
|
import { cliLogger } from '../utils/logger.js';
|
|
@@ -12,9 +12,10 @@ import { cliLogger } from '../utils/logger.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export async function runBuild(options) {
|
|
14
14
|
const domainRoot = resolve(options.domainRoot);
|
|
15
|
+
const domainId = basename(domainRoot);
|
|
15
16
|
const outFile = options.outFile
|
|
16
17
|
? resolve(options.outFile)
|
|
17
|
-
: join(process.cwd(), '.tib',
|
|
18
|
+
: join(process.cwd(), '.tib', `${domainId}-registry.json`);
|
|
18
19
|
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
19
20
|
const { registry, warnings } = await buildRegistry(domainRoot);
|
|
20
21
|
for (const w of warnings) {
|
|
@@ -117,9 +117,48 @@ export async function runValidate(domainRoot, exitOnFailure = true, config = { m
|
|
|
117
117
|
}
|
|
118
118
|
}));
|
|
119
119
|
errors.push(...checkSubscriberSemverRanges(registry.subscribers));
|
|
120
|
+
const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
120
121
|
for (const api of registry.apis) {
|
|
121
122
|
const entry = api;
|
|
122
123
|
errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
|
|
124
|
+
if (!api.requestSchema) {
|
|
125
|
+
errors.push({
|
|
126
|
+
code: 'MISSING_API_REQUEST_SCHEMA',
|
|
127
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has no request schema. Define input/output in defineApi versions.`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (!api.responseSchema) {
|
|
131
|
+
errors.push({
|
|
132
|
+
code: 'MISSING_API_RESPONSE_SCHEMA',
|
|
133
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has no response schema. Define input/output in defineApi versions.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (!VALID_METHODS.has(api.method)) {
|
|
137
|
+
errors.push({
|
|
138
|
+
code: 'INVALID_API_METHOD',
|
|
139
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has invalid method "${api.method}". Use one of: ${[...VALID_METHODS].join(', ')}.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function hasExampleOrDefault(schema) {
|
|
143
|
+
if (!schema)
|
|
144
|
+
return false;
|
|
145
|
+
if (schema['default'] !== undefined)
|
|
146
|
+
return true;
|
|
147
|
+
const examples = schema['examples'] ?? schema['example'];
|
|
148
|
+
return Array.isArray(examples) && examples.length > 0;
|
|
149
|
+
}
|
|
150
|
+
if (api.requestSchema && !hasExampleOrDefault(api.requestSchema)) {
|
|
151
|
+
errors.push({
|
|
152
|
+
code: 'MISSING_API_REQUEST_EXAMPLE',
|
|
153
|
+
message: `API '${api.id}' (${api.method} ${api.path}) request schema has no example data. Add .default({...}) to your Zod schema.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (api.responseSchema && !hasExampleOrDefault(api.responseSchema)) {
|
|
157
|
+
errors.push({
|
|
158
|
+
code: 'MISSING_API_RESPONSE_EXAMPLE',
|
|
159
|
+
message: `API '${api.id}' (${api.method} ${api.path}) response schema has no example data. Add .default({...}) to your Zod schema.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
123
162
|
}
|
|
124
163
|
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|
|
125
164
|
errors.push(...rawPathErrors);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mettlecast/domain-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -21,21 +21,21 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
23
23
|
"@aws-sdk/client-sfn": "^3.0.0",
|
|
24
|
-
"@mettlecast/domain-runtime": "*",
|
|
25
24
|
"@mettlecast/domain-cdk-packer": "*",
|
|
25
|
+
"@mettlecast/domain-runtime": "*",
|
|
26
26
|
"commander": "^12.0.0",
|
|
27
|
+
"dotenv": "^16.0.0",
|
|
27
28
|
"fastify": "^5.0.0",
|
|
28
|
-
"tar": "^7.0.0",
|
|
29
|
-
"tsx": "^4.0.0",
|
|
30
|
-
"zod-to-json-schema": "^3.0.0",
|
|
31
29
|
"pino": "^9.0.0",
|
|
32
30
|
"pino-pretty": "^11.0.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
31
|
+
"semver": "^7.0.0",
|
|
32
|
+
"tar": "^7.0.0",
|
|
33
|
+
"tsx": "^4.0.0",
|
|
34
|
+
"zod-to-json-schema": "^3.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20.0.0",
|
|
38
|
-
"@types/semver": "^7.
|
|
38
|
+
"@types/semver": "^7.7.1",
|
|
39
39
|
"@types/tar": "^6.0.0",
|
|
40
40
|
"typescript": "^5.0.0",
|
|
41
41
|
"vitest": "^2.0.0"
|
|
@@ -99,10 +99,16 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
99
99
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
100
100
|
]);
|
|
101
101
|
|
|
102
|
+
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
103
|
+
|
|
102
104
|
const apis: ApiRegistryEntry[] = paths.apis.flatMap((filePath, i) =>
|
|
103
105
|
(apiExports[i] ?? [])
|
|
104
106
|
.filter(e => e['_kind'] === 'api')
|
|
105
107
|
.map(e => {
|
|
108
|
+
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
109
|
+
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
110
|
+
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
111
|
+
}
|
|
106
112
|
const rawVersions = e['versions'] as Record<string, { input?: unknown; output?: unknown }> | undefined;
|
|
107
113
|
const versionSnapshots: ApiVersionSnapshot[] = rawVersions
|
|
108
114
|
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
@@ -117,7 +123,7 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
117
123
|
kind: 'api' as const,
|
|
118
124
|
handlerFile: relPath(filePath),
|
|
119
125
|
path: String(e['path']),
|
|
120
|
-
method:
|
|
126
|
+
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
121
127
|
authType: ((e['auth'] as { type?: string } | undefined)?.type as 'jwt' | 'api-key' | 'none' | undefined) ?? 'jwt',
|
|
122
128
|
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
123
129
|
deployment: deployment(e),
|
|
@@ -43,20 +43,23 @@ function makeEvalScript(absoluteFilePath: string): string {
|
|
|
43
43
|
// Convert to file:// URL so ESM loader works on all platforms (especially Windows).
|
|
44
44
|
const fileUrl = pathToFileURL(absoluteFilePath).href;
|
|
45
45
|
return `
|
|
46
|
-
import {
|
|
46
|
+
import { z } from 'zod';
|
|
47
47
|
import * as mod from ${JSON.stringify(fileUrl)};
|
|
48
48
|
const KINDS = new Set(['api','webhook','subscriber','schedule','job','action','integration','event','domain']);
|
|
49
49
|
function isZod(v) {
|
|
50
50
|
return v && typeof v === 'object' && typeof v.parse === 'function' && typeof v.safeParse === 'function' && v._def !== undefined;
|
|
51
51
|
}
|
|
52
|
+
function toJsonSchema(v) {
|
|
53
|
+
try { return z.toJSONSchema(v); } catch { /* skip unserializable */ }
|
|
54
|
+
}
|
|
52
55
|
function strip(obj) {
|
|
53
56
|
const out = {};
|
|
54
57
|
for (const [k, v] of Object.entries(obj)) {
|
|
55
58
|
if (typeof v === 'function') continue;
|
|
56
59
|
if (isZod(v)) {
|
|
57
|
-
|
|
60
|
+
out[k] = toJsonSchema(v);
|
|
58
61
|
} else if (Array.isArray(v)) {
|
|
59
|
-
out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? (
|
|
62
|
+
out[k] = v.map(item => (item && typeof item === 'object' && !isZod(item) ? strip(item) : isZod(item) ? toJsonSchema(item) : item));
|
|
60
63
|
} else if (v && typeof v === 'object') {
|
|
61
64
|
out[k] = strip(v);
|
|
62
65
|
} else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { cliLogger } from '../utils/logger.js';
|
|
4
4
|
|
|
@@ -43,6 +43,32 @@ export interface CatalogSubscriber {
|
|
|
43
43
|
description?: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** A single job entry in the catalog. */
|
|
47
|
+
export interface CatalogJob {
|
|
48
|
+
id: string;
|
|
49
|
+
domainId: string;
|
|
50
|
+
maxRetries: number;
|
|
51
|
+
visibilityTimeoutSeconds: number;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A single schedule entry in the catalog. */
|
|
56
|
+
export interface CatalogSchedule {
|
|
57
|
+
id: string;
|
|
58
|
+
domainId: string;
|
|
59
|
+
cron: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A single integration entry in the catalog. */
|
|
65
|
+
export interface CatalogIntegration {
|
|
66
|
+
id: string;
|
|
67
|
+
domainId: string;
|
|
68
|
+
baseUrl: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
/** A single domain entry in the catalog. */
|
|
47
73
|
export interface CatalogDomain {
|
|
48
74
|
id: string;
|
|
@@ -60,6 +86,9 @@ export interface DomainCatalog {
|
|
|
60
86
|
actions: CatalogAction[];
|
|
61
87
|
events: CatalogEvent[];
|
|
62
88
|
subscribers: CatalogSubscriber[];
|
|
89
|
+
jobs: CatalogJob[];
|
|
90
|
+
schedules: CatalogSchedule[];
|
|
91
|
+
integrations: CatalogIntegration[];
|
|
63
92
|
}
|
|
64
93
|
|
|
65
94
|
/**
|
|
@@ -92,6 +121,9 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
|
|
|
92
121
|
actions: [],
|
|
93
122
|
events: [],
|
|
94
123
|
subscribers: [],
|
|
124
|
+
jobs: [],
|
|
125
|
+
schedules: [],
|
|
126
|
+
integrations: [],
|
|
95
127
|
};
|
|
96
128
|
|
|
97
129
|
for (const file of registryFiles) {
|
|
@@ -167,13 +199,47 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
|
|
|
167
199
|
description: sub['description'] as string | undefined,
|
|
168
200
|
});
|
|
169
201
|
}
|
|
202
|
+
|
|
203
|
+
const jobs = (registry['jobs'] as Record<string, unknown>[] | undefined) ?? [];
|
|
204
|
+
for (const job of jobs) {
|
|
205
|
+
catalog.jobs.push({
|
|
206
|
+
id: String(job['id']),
|
|
207
|
+
domainId,
|
|
208
|
+
maxRetries: Number(job['maxRetries'] ?? 3),
|
|
209
|
+
visibilityTimeoutSeconds: Number(job['visibilityTimeoutSeconds'] ?? 300),
|
|
210
|
+
description: job['description'] as string | undefined,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const schedules = (registry['schedules'] as Record<string, unknown>[] | undefined) ?? [];
|
|
215
|
+
for (const schedule of schedules) {
|
|
216
|
+
catalog.schedules.push({
|
|
217
|
+
id: String(schedule['id']),
|
|
218
|
+
domainId,
|
|
219
|
+
cron: String(schedule['cron'] ?? ''),
|
|
220
|
+
enabled: Boolean(schedule['enabled'] ?? true),
|
|
221
|
+
description: schedule['description'] as string | undefined,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const integrations = (registry['integrations'] as Record<string, unknown>[] | undefined) ?? [];
|
|
226
|
+
for (const integration of integrations) {
|
|
227
|
+
catalog.integrations.push({
|
|
228
|
+
id: String(integration['id']),
|
|
229
|
+
domainId,
|
|
230
|
+
baseUrl: String(integration['baseUrl'] ?? ''),
|
|
231
|
+
description: integration['description'] as string | undefined,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
170
234
|
}
|
|
171
235
|
|
|
172
|
-
const
|
|
236
|
+
const mcDir = join(process.cwd(), '.mc');
|
|
237
|
+
await mkdir(mcDir, { recursive: true });
|
|
238
|
+
const outPath = join(mcDir, 'domain-registry.json');
|
|
173
239
|
await writeFile(outPath, JSON.stringify(catalog, null, 2), 'utf8');
|
|
174
240
|
|
|
175
241
|
cliLogger.info(
|
|
176
|
-
{ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length },
|
|
242
|
+
{ outPath, domains: catalog.domains.length, apis: catalog.apis.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
|
|
177
243
|
'Domain catalog written'
|
|
178
244
|
);
|
|
179
245
|
|
package/src/commands/build.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
2
|
+
import { join, resolve, basename } from 'node:path';
|
|
3
3
|
import type { DomainRegistry } from '@mettlecast/domain-cdk-packer';
|
|
4
4
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
5
5
|
import { buildActionsTypes } from '../builder/build-types.js';
|
|
@@ -24,9 +24,10 @@ export interface BuildOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
export async function runBuild(options: BuildOptions): Promise<string> {
|
|
26
26
|
const domainRoot = resolve(options.domainRoot);
|
|
27
|
+
const domainId = basename(domainRoot);
|
|
27
28
|
const outFile = options.outFile
|
|
28
29
|
? resolve(options.outFile)
|
|
29
|
-
: join(process.cwd(), '.tib',
|
|
30
|
+
: join(process.cwd(), '.tib', `${domainId}-registry.json`);
|
|
30
31
|
|
|
31
32
|
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
32
33
|
|
package/src/commands/validate.ts
CHANGED
|
@@ -173,9 +173,46 @@ export async function runValidate(
|
|
|
173
173
|
|
|
174
174
|
errors.push(...checkSubscriberSemverRanges(registry.subscribers));
|
|
175
175
|
|
|
176
|
+
const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
176
177
|
for (const api of registry.apis) {
|
|
177
178
|
const entry = api as unknown as { deprecatedAt?: string; sunsetAt?: string };
|
|
178
179
|
errors.push(...checkLifecycleConsistency(entry, `api '${api.id}'`));
|
|
180
|
+
if (!api.requestSchema) {
|
|
181
|
+
errors.push({
|
|
182
|
+
code: 'MISSING_API_REQUEST_SCHEMA',
|
|
183
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has no request schema. Define input/output in defineApi versions.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (!api.responseSchema) {
|
|
187
|
+
errors.push({
|
|
188
|
+
code: 'MISSING_API_RESPONSE_SCHEMA',
|
|
189
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has no response schema. Define input/output in defineApi versions.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!VALID_METHODS.has(api.method)) {
|
|
193
|
+
errors.push({
|
|
194
|
+
code: 'INVALID_API_METHOD',
|
|
195
|
+
message: `API '${api.id}' (${api.method} ${api.path}) has invalid method "${api.method}". Use one of: ${[...VALID_METHODS].join(', ')}.`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function hasExampleOrDefault(schema: Record<string, unknown> | undefined): boolean {
|
|
199
|
+
if (!schema) return false;
|
|
200
|
+
if (schema['default'] !== undefined) return true;
|
|
201
|
+
const examples = schema['examples'] ?? schema['example'];
|
|
202
|
+
return Array.isArray(examples) && examples.length > 0;
|
|
203
|
+
}
|
|
204
|
+
if (api.requestSchema && !hasExampleOrDefault(api.requestSchema)) {
|
|
205
|
+
errors.push({
|
|
206
|
+
code: 'MISSING_API_REQUEST_EXAMPLE',
|
|
207
|
+
message: `API '${api.id}' (${api.method} ${api.path}) request schema has no example data. Add .default({...}) to your Zod schema.`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (api.responseSchema && !hasExampleOrDefault(api.responseSchema)) {
|
|
211
|
+
errors.push({
|
|
212
|
+
code: 'MISSING_API_RESPONSE_EXAMPLE',
|
|
213
|
+
message: `API '${api.id}' (${api.method} ${api.path}) response schema has no example data. Add .default({...}) to your Zod schema.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
179
216
|
}
|
|
180
217
|
|
|
181
218
|
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|