more-apartments-astro-integration 2.0.1 ā 2.0.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/package.json +2 -1
- package/scripts/detect-breaking-changes.mjs +199 -0
- package/scripts/fetch-staging-spec.mjs +81 -0
- package/scripts/generate-types.mjs +140 -0
- package/scripts/generate-zod-schemas.mjs +200 -0
- package/scripts/setup-test-api-key.js +112 -0
- package/scripts/validate-schemas.mjs +123 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "more-apartments-astro-integration",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Astro integration for More Apartments REST API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
23
|
"routes",
|
|
24
|
+
"scripts",
|
|
24
25
|
"src/components",
|
|
25
26
|
"src/pages",
|
|
26
27
|
".more-apartments"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect breaking changes by comparing OpenAPI specs
|
|
5
|
+
* Suggests appropriate version bump (major/minor/patch)
|
|
6
|
+
*
|
|
7
|
+
* This script analyzes differences between the current and previous
|
|
8
|
+
* OpenAPI specifications to determine if breaking changes have been
|
|
9
|
+
* introduced, helping maintain proper semantic versioning.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const CURRENT_SPEC = resolve(__dirname, '../.openapi/api-v1.json');
|
|
20
|
+
const PREVIOUS_SPEC = resolve(__dirname, '../.openapi/api-v1.previous.json');
|
|
21
|
+
|
|
22
|
+
async function analyzeChanges() {
|
|
23
|
+
console.log('š Analyzing API changes for breaking compatibility...\n');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(CURRENT_SPEC)) {
|
|
26
|
+
console.error('ā Current OpenAPI spec not found:', CURRENT_SPEC);
|
|
27
|
+
console.log('SUGGESTED_BUMP=patch');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!existsSync(PREVIOUS_SPEC)) {
|
|
32
|
+
console.log('š No previous spec found. This appears to be a new version.');
|
|
33
|
+
console.log(' Future runs will compare against this spec.');
|
|
34
|
+
console.log('\nSUGGESTED_BUMP=minor');
|
|
35
|
+
|
|
36
|
+
// Set output for GitHub Actions
|
|
37
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
38
|
+
const { appendFileSync } = await import('fs');
|
|
39
|
+
appendFileSync(process.env.GITHUB_OUTPUT, 'suggested_bump=minor\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const current = JSON.parse(readFileSync(CURRENT_SPEC, 'utf-8'));
|
|
46
|
+
const previous = JSON.parse(readFileSync(PREVIOUS_SPEC, 'utf-8'));
|
|
47
|
+
|
|
48
|
+
const changes = {
|
|
49
|
+
breaking: [],
|
|
50
|
+
additions: [],
|
|
51
|
+
modifications: [],
|
|
52
|
+
deprecations: []
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 1. Check for removed endpoints
|
|
56
|
+
for (const path in previous.paths) {
|
|
57
|
+
if (!current.paths[path]) {
|
|
58
|
+
changes.breaking.push(`Removed endpoint: ${path}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Check for removed or modified operations
|
|
63
|
+
for (const path in current.paths) {
|
|
64
|
+
if (!previous.paths[path]) {
|
|
65
|
+
changes.additions.push(`New endpoint: ${path}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const method in current.paths[path]) {
|
|
70
|
+
const currentOp = current.paths[path][method];
|
|
71
|
+
const previousOp = previous.paths[path]?.[method];
|
|
72
|
+
|
|
73
|
+
if (!previousOp) {
|
|
74
|
+
changes.additions.push(`New method: ${method.toUpperCase()} ${path}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Check for parameter changes
|
|
79
|
+
const prevParams = previousOp.parameters || [];
|
|
80
|
+
const currParams = currentOp.parameters || [];
|
|
81
|
+
|
|
82
|
+
// Check for removed required parameters (BREAKING)
|
|
83
|
+
for (const prevParam of prevParams) {
|
|
84
|
+
const stillExists = currParams.find(p => p.name === prevParam.name && p.in === prevParam.in);
|
|
85
|
+
|
|
86
|
+
if (!stillExists) {
|
|
87
|
+
if (prevParam.required) {
|
|
88
|
+
changes.breaking.push(
|
|
89
|
+
`Removed required parameter: ${prevParam.name} from ${method.toUpperCase()} ${path}`
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
changes.modifications.push(
|
|
93
|
+
`Removed optional parameter: ${prevParam.name} from ${method.toUpperCase()} ${path}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (prevParam.required && !stillExists.required) {
|
|
97
|
+
changes.modifications.push(
|
|
98
|
+
`Parameter ${prevParam.name} is now optional in ${method.toUpperCase()} ${path}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for new required parameters (BREAKING)
|
|
104
|
+
for (const currParam of currParams) {
|
|
105
|
+
const existed = prevParams.find(p => p.name === currParam.name && p.in === currParam.in);
|
|
106
|
+
|
|
107
|
+
if (!existed && currParam.required) {
|
|
108
|
+
changes.breaking.push(
|
|
109
|
+
`New required parameter: ${currParam.name} in ${method.toUpperCase()} ${path}`
|
|
110
|
+
);
|
|
111
|
+
} else if (!existed) {
|
|
112
|
+
changes.additions.push(
|
|
113
|
+
`New optional parameter: ${currParam.name} in ${method.toUpperCase()} ${path}`
|
|
114
|
+
);
|
|
115
|
+
} else if (!existed.required && currParam.required) {
|
|
116
|
+
changes.breaking.push(
|
|
117
|
+
`Parameter ${currParam.name} is now required in ${method.toUpperCase()} ${path}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 4. Check for response schema changes (simplified check)
|
|
123
|
+
if (currentOp.responses && previousOp.responses) {
|
|
124
|
+
const prev200 = previousOp.responses['200'];
|
|
125
|
+
const curr200 = currentOp.responses['200'];
|
|
126
|
+
|
|
127
|
+
if (prev200 && !curr200) {
|
|
128
|
+
changes.breaking.push(`Removed 200 response from ${method.toUpperCase()} ${path}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determine version bump based on changes
|
|
135
|
+
let suggestedBump = 'patch';
|
|
136
|
+
|
|
137
|
+
console.log('š Change Summary:\n');
|
|
138
|
+
console.log('ā'.repeat(60));
|
|
139
|
+
|
|
140
|
+
if (changes.breaking.length > 0) {
|
|
141
|
+
suggestedBump = 'major';
|
|
142
|
+
console.log('šØ BREAKING CHANGES DETECTED:\n');
|
|
143
|
+
changes.breaking.forEach(c => console.log(` ā ${c}`));
|
|
144
|
+
console.log('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (changes.additions.length > 0 && suggestedBump !== 'major') {
|
|
148
|
+
suggestedBump = 'minor';
|
|
149
|
+
console.log('⨠New Features:\n');
|
|
150
|
+
changes.additions.forEach(c => console.log(` ā
${c}`));
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (changes.modifications.length > 0) {
|
|
155
|
+
console.log('š§ Modifications:\n');
|
|
156
|
+
changes.modifications.forEach(c => console.log(` š ${c}`));
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (changes.deprecations.length > 0) {
|
|
161
|
+
console.log('ā ļø Deprecations:\n');
|
|
162
|
+
changes.deprecations.forEach(c => console.log(` ā³ ${c}`));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('ā'.repeat(60));
|
|
167
|
+
console.log(`\nš¦ RECOMMENDED VERSION BUMP: ${suggestedBump.toUpperCase()}`);
|
|
168
|
+
|
|
169
|
+
if (suggestedBump === 'major') {
|
|
170
|
+
console.log('ā ļø This is a MAJOR version bump due to breaking changes.');
|
|
171
|
+
console.log(' Consumers will need to update their code.');
|
|
172
|
+
} else if (suggestedBump === 'minor') {
|
|
173
|
+
console.log('ā
This is a MINOR version bump (new features, backwards compatible).');
|
|
174
|
+
} else {
|
|
175
|
+
console.log('ā
This is a PATCH version bump (bug fixes, backwards compatible).');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`\nSUGGESTED_BUMP=${suggestedBump}`);
|
|
179
|
+
|
|
180
|
+
// Set output for GitHub Actions
|
|
181
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
182
|
+
const { appendFileSync } = await import('fs');
|
|
183
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `suggested_bump=${suggestedBump}\n`);
|
|
184
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `breaking_count=${changes.breaking.length}\n`);
|
|
185
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `feature_count=${changes.additions.length}\n`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Exit with error code if breaking changes detected (optional strict mode)
|
|
189
|
+
if (process.argv.includes('--strict') && changes.breaking.length > 0) {
|
|
190
|
+
console.error('\nā Breaking changes detected in strict mode!');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
analyzeChanges().catch(error => {
|
|
196
|
+
console.error('ā Error analyzing changes:', error.message);
|
|
197
|
+
console.log('\nSUGGESTED_BUMP=patch');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch OpenAPI spec from GitHub Releases (Staging/Pre-release)
|
|
5
|
+
*
|
|
6
|
+
* This script fetches the latest staging pre-release from GitHub Releases.
|
|
7
|
+
* Staging releases are marked with --prerelease flag and have "staging" in the tag.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun run fetch:spec:staging
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
15
|
+
import { resolve, dirname } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
const GITHUB_REPO = 'Feelgood-Apartments/prj-more-apartments';
|
|
22
|
+
const OPENAPI_DEST = resolve(__dirname, '../.openapi/api-v1.json');
|
|
23
|
+
|
|
24
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
25
|
+
console.log('š Fetching Staging OpenAPI Spec');
|
|
26
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Ensure .openapi directory exists
|
|
30
|
+
const openapiDir = dirname(OPENAPI_DEST);
|
|
31
|
+
if (!existsSync(openapiDir)) {
|
|
32
|
+
mkdirSync(openapiDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get latest staging pre-release tag
|
|
36
|
+
console.log('š” Fetching latest staging pre-release from GitHub...');
|
|
37
|
+
|
|
38
|
+
const getLatestStagingTag = `gh release list --repo ${GITHUB_REPO} --limit 50 --json tagName,isPrerelease | jq -r '.[] | select(.isPrerelease == true and (.tagName | contains("staging"))) | .tagName' | head -1`;
|
|
39
|
+
|
|
40
|
+
let latestTag;
|
|
41
|
+
try {
|
|
42
|
+
latestTag = execSync(getLatestStagingTag, { encoding: 'utf-8' }).trim();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('\nā Failed to fetch staging release information');
|
|
45
|
+
console.error(' Make sure you have GitHub CLI (gh) and jq installed:');
|
|
46
|
+
console.error(' brew install gh jq');
|
|
47
|
+
console.error('\n Or use production spec: bun run fetch:spec');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!latestTag) {
|
|
52
|
+
console.error('\nā No staging pre-releases found');
|
|
53
|
+
console.error(' Use production spec instead: bun run fetch:spec');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`ā
Latest staging release: ${latestTag}`);
|
|
58
|
+
|
|
59
|
+
// Download the spec from the staging release
|
|
60
|
+
console.log('\nš„ Downloading OpenAPI spec...');
|
|
61
|
+
|
|
62
|
+
// Use gh CLI for authenticated downloads from private repositories
|
|
63
|
+
execSync(`gh release download ${latestTag} --repo ${GITHUB_REPO} --pattern "api-v1.json" --output "${OPENAPI_DEST}" --clobber`, {
|
|
64
|
+
stdio: 'inherit'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`ā
Staging spec downloaded to: ${OPENAPI_DEST}`);
|
|
68
|
+
|
|
69
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
70
|
+
console.log('⨠Staging OpenAPI spec fetched successfully!');
|
|
71
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
72
|
+
console.log(`\nš” Next steps:`);
|
|
73
|
+
console.log(` 1. Generate TypeScript types: bun run generate:types:local`);
|
|
74
|
+
console.log(` 2. Generate Zod schemas: bun run generate:schemas`);
|
|
75
|
+
console.log(` 3. Build package: bun run build\n`);
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('\nā Failed to fetch staging OpenAPI spec');
|
|
79
|
+
console.error(error.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate TypeScript types from OpenAPI specification (LOCAL DEVELOPMENT ONLY)
|
|
5
|
+
*
|
|
6
|
+
* This script is for local development when you have the Laravel project available.
|
|
7
|
+
* It exports the OpenAPI spec from your local Laravel backend and generates types.
|
|
8
|
+
*
|
|
9
|
+
* For CI/CD builds, use the fetch:spec + generate:types:local workflow instead.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bun run generate:types (local Laravel export + type generation)
|
|
13
|
+
* bun run dev:local (watch mode for local development)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import { resolve, dirname } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { existsSync, mkdirSync, copyFileSync } from 'fs';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// Configuration
|
|
25
|
+
const LARAVEL_PROJECT_PATH = process.env.MORE_APARTMENTS_PROJECT_PATH || '../prj-more-apartments';
|
|
26
|
+
const OPENAPI_SOURCE = resolve(LARAVEL_PROJECT_PATH, 'api-v1.json');
|
|
27
|
+
const OPENAPI_DEST = resolve(__dirname, '../.openapi/api-v1.json');
|
|
28
|
+
const TYPES_OUTPUT = resolve(__dirname, '../src/types/generated/api.d.ts');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run a command and log output
|
|
32
|
+
*/
|
|
33
|
+
function run(command, cwd = process.cwd()) {
|
|
34
|
+
console.log(`\nš§ Running: ${command}`);
|
|
35
|
+
console.log(` Working directory: ${cwd}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
execSync(command, {
|
|
39
|
+
cwd,
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
env: process.env,
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`\nā Command failed: ${command}`);
|
|
46
|
+
console.error(error.message);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Main execution
|
|
53
|
+
*/
|
|
54
|
+
async function main() {
|
|
55
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
56
|
+
console.log('š Local Development Type Generation');
|
|
57
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
58
|
+
|
|
59
|
+
// Step 1: Export OpenAPI spec from local Laravel project
|
|
60
|
+
console.log('š¤ Step 1: Exporting OpenAPI spec from local Laravel project...');
|
|
61
|
+
|
|
62
|
+
const laravelPath = resolve(__dirname, LARAVEL_PROJECT_PATH);
|
|
63
|
+
|
|
64
|
+
if (!existsSync(laravelPath)) {
|
|
65
|
+
console.error(`\nā Laravel project not found at: ${laravelPath}`);
|
|
66
|
+
console.error(` Expected path: ${laravelPath}`);
|
|
67
|
+
console.error('\n For CI/CD builds, use:');
|
|
68
|
+
console.error(' bun run fetch:spec && bun run generate:types:local');
|
|
69
|
+
console.error('\n Set MORE_APARTMENTS_PROJECT_PATH if your Laravel project is elsewhere');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const success = run(`php artisan scramble:export --path=api-v1.json`, laravelPath);
|
|
74
|
+
|
|
75
|
+
if (!success) {
|
|
76
|
+
console.error('\nā Failed to export OpenAPI spec from Laravel');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!existsSync(OPENAPI_SOURCE)) {
|
|
81
|
+
console.error(`\nā OpenAPI spec not found at: ${OPENAPI_SOURCE}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`ā
OpenAPI spec exported to: ${OPENAPI_SOURCE}`);
|
|
86
|
+
|
|
87
|
+
// Step 2: Copy OpenAPI spec to integration package
|
|
88
|
+
console.log('\nš Step 2: Copying OpenAPI spec to integration package...');
|
|
89
|
+
|
|
90
|
+
const openapiDir = dirname(OPENAPI_DEST);
|
|
91
|
+
if (!existsSync(openapiDir)) {
|
|
92
|
+
mkdirSync(openapiDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
copyFileSync(OPENAPI_SOURCE, OPENAPI_DEST);
|
|
96
|
+
console.log(`ā
Copied to: ${OPENAPI_DEST}`);
|
|
97
|
+
|
|
98
|
+
// Step 3: Generate TypeScript types
|
|
99
|
+
console.log('\nšÆ Step 3: Generating TypeScript types...');
|
|
100
|
+
|
|
101
|
+
const typesDir = dirname(TYPES_OUTPUT);
|
|
102
|
+
if (!existsSync(typesDir)) {
|
|
103
|
+
mkdirSync(typesDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const success = run(
|
|
107
|
+
`npx openapi-typescript ${OPENAPI_DEST} --output ${TYPES_OUTPUT}`,
|
|
108
|
+
resolve(__dirname, '..')
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!success) {
|
|
112
|
+
console.error('\nā Failed to generate TypeScript types');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!existsSync(TYPES_OUTPUT)) {
|
|
117
|
+
console.error(`\nā Generated types not found at: ${TYPES_OUTPUT}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`ā
Types generated at: ${TYPES_OUTPUT}`);
|
|
122
|
+
|
|
123
|
+
// Summary
|
|
124
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
125
|
+
console.log('⨠Local development types generated!');
|
|
126
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
127
|
+
console.log(`\nš Generated files:`);
|
|
128
|
+
console.log(` - ${OPENAPI_DEST}`);
|
|
129
|
+
console.log(` - ${TYPES_OUTPUT}`);
|
|
130
|
+
console.log(`\nš” Next steps:`);
|
|
131
|
+
console.log(` 1. Generate Zod schemas: bun run generate:schemas`);
|
|
132
|
+
console.log(` 2. Run tests: bun test`);
|
|
133
|
+
console.log(` 3. Build package: bun run build`);
|
|
134
|
+
console.log(`\nš For watch mode: bun run dev:local\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main().catch((error) => {
|
|
138
|
+
console.error('\nā Fatal error:', error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate Zod schemas from OpenAPI specification
|
|
5
|
+
*
|
|
6
|
+
* This script parses the OpenAPI spec and generates:
|
|
7
|
+
* - Consolidated Zod object schemas for query parameters
|
|
8
|
+
* - Zod schemas for request/response bodies
|
|
9
|
+
* - Exports for use in client.ts
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bun run scripts/generate-zod-schemas.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
16
|
+
import { resolve, dirname } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
const OPENAPI_SOURCE = resolve(__dirname, '../.openapi/api-v1.json');
|
|
24
|
+
const OUTPUT_FILE = resolve(__dirname, '../src/types/generated/schemas.ts');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert OpenAPI type to Zod schema
|
|
28
|
+
*/
|
|
29
|
+
function openapiTypeToZod(param) {
|
|
30
|
+
const { schema, required } = param;
|
|
31
|
+
|
|
32
|
+
if (!schema) return 'z.unknown()';
|
|
33
|
+
|
|
34
|
+
let zodType;
|
|
35
|
+
|
|
36
|
+
// Handle OpenAPI 3.1 array type format: ["string", "null"]
|
|
37
|
+
let type = schema.type;
|
|
38
|
+
let isNullable = false;
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(type)) {
|
|
41
|
+
isNullable = type.includes('null');
|
|
42
|
+
type = type.find(t => t !== 'null') || type[0];
|
|
43
|
+
} else if (schema.nullable) {
|
|
44
|
+
isNullable = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle type
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'string':
|
|
50
|
+
if (schema.format === 'date' || schema.format === 'date-time') {
|
|
51
|
+
zodType = 'z.string()';
|
|
52
|
+
} else if (schema.enum) {
|
|
53
|
+
const enumValues = schema.enum.map(v => `"${v}"`).join(', ');
|
|
54
|
+
zodType = `z.enum([${enumValues}])`;
|
|
55
|
+
} else {
|
|
56
|
+
zodType = 'z.string()';
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'integer':
|
|
61
|
+
case 'number':
|
|
62
|
+
zodType = 'z.number()';
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'boolean':
|
|
66
|
+
zodType = 'z.boolean()';
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'array':
|
|
70
|
+
const itemType = schema.items ? openapiTypeToZod({ schema: schema.items, required: true }) : 'z.unknown()';
|
|
71
|
+
zodType = `z.array(${itemType})`;
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'object':
|
|
75
|
+
zodType = 'z.object({})';
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
default:
|
|
79
|
+
zodType = 'z.unknown()';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle nullable (from array type or nullable property)
|
|
83
|
+
if (isNullable) {
|
|
84
|
+
zodType = `${zodType}.nullable()`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle optional (if not required)
|
|
88
|
+
if (!required) {
|
|
89
|
+
zodType = `${zodType}.optional()`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return zodType;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate schemas from OpenAPI spec
|
|
97
|
+
*/
|
|
98
|
+
function generateSchemas() {
|
|
99
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
100
|
+
console.log('š Zod Schema Generation from OpenAPI');
|
|
101
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
102
|
+
|
|
103
|
+
// Read OpenAPI spec
|
|
104
|
+
console.log(`š Reading OpenAPI spec from: ${OPENAPI_SOURCE}`);
|
|
105
|
+
const openapi = JSON.parse(readFileSync(OPENAPI_SOURCE, 'utf-8'));
|
|
106
|
+
|
|
107
|
+
// Start building output
|
|
108
|
+
let output = `// Generated by scripts/generate-zod-schemas.mjs
|
|
109
|
+
// DO NOT EDIT MANUALLY - This file is auto-generated from OpenAPI spec
|
|
110
|
+
|
|
111
|
+
import { z } from 'zod';
|
|
112
|
+
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const schemas = [];
|
|
116
|
+
|
|
117
|
+
// Process each path/operation
|
|
118
|
+
for (const [path, pathItem] of Object.entries(openapi.paths)) {
|
|
119
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
120
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) continue;
|
|
121
|
+
|
|
122
|
+
const operationId = operation.operationId;
|
|
123
|
+
if (!operationId) continue;
|
|
124
|
+
|
|
125
|
+
// Extract query parameters
|
|
126
|
+
const queryParams = (operation.parameters || [])
|
|
127
|
+
.filter(p => p.in === 'query');
|
|
128
|
+
|
|
129
|
+
if (queryParams.length > 0) {
|
|
130
|
+
const schemaName = toCamelCase(operationId) + 'ParamsSchema';
|
|
131
|
+
const properties = queryParams.map(param => {
|
|
132
|
+
const zodType = openapiTypeToZod(param);
|
|
133
|
+
const comment = param.description ? ` // ${param.description}\n` : '';
|
|
134
|
+
return `${comment} ${param.name}: ${zodType}`;
|
|
135
|
+
}).join(',\n');
|
|
136
|
+
|
|
137
|
+
const schema = `/**
|
|
138
|
+
* Query parameters for ${operationId}
|
|
139
|
+
* Generated from OpenAPI operation: ${method.toUpperCase()} ${path}
|
|
140
|
+
*/
|
|
141
|
+
export const ${schemaName} = z.object({
|
|
142
|
+
${properties}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export type ${toCamelCase(operationId)}Params = z.infer<typeof ${schemaName}>;
|
|
146
|
+
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
schemas.push(schema);
|
|
150
|
+
console.log(`ā
Generated schema: ${schemaName} (${queryParams.length} parameters)`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Extract request body schema
|
|
154
|
+
if (operation.requestBody?.content?.['application/json']?.schema) {
|
|
155
|
+
const bodySchema = operation.requestBody.content['application/json'].schema;
|
|
156
|
+
const schemaName = toCamelCase(operationId) + 'RequestSchema';
|
|
157
|
+
|
|
158
|
+
// For now, just reference the schema (detailed generation would go here)
|
|
159
|
+
if (bodySchema.$ref) {
|
|
160
|
+
const refName = bodySchema.$ref.split('/').pop();
|
|
161
|
+
schemas.push(`// Request body schema for ${operationId} references: ${refName}\n`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output += schemas.join('\n');
|
|
168
|
+
|
|
169
|
+
// Write output file
|
|
170
|
+
console.log(`\nš Writing schemas to: ${OUTPUT_FILE}`);
|
|
171
|
+
writeFileSync(OUTPUT_FILE, output, 'utf-8');
|
|
172
|
+
|
|
173
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
174
|
+
console.log('⨠Zod schema generation complete!');
|
|
175
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
176
|
+
console.log(`\nš Generated file: ${OUTPUT_FILE}`);
|
|
177
|
+
console.log(`š Total schemas: ${schemas.length}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Convert string to CamelCase
|
|
182
|
+
*/
|
|
183
|
+
function toCamelCase(str) {
|
|
184
|
+
return str
|
|
185
|
+
.split(/[.\-_]/)
|
|
186
|
+
.map((word, index) =>
|
|
187
|
+
index === 0
|
|
188
|
+
? word.charAt(0).toUpperCase() + word.slice(1)
|
|
189
|
+
: word.charAt(0).toUpperCase() + word.slice(1)
|
|
190
|
+
)
|
|
191
|
+
.join('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Run generator
|
|
195
|
+
try {
|
|
196
|
+
generateSchemas();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('\nā Fatal error:', error);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test API Key Setup Script
|
|
5
|
+
*
|
|
6
|
+
* This script automatically generates a test API key from your Laravel project
|
|
7
|
+
* and configures the integration tests to use it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const LARAVEL_PROJECT_PATH = '/Users/shelfwood/Projects/prj-more-apartments';
|
|
15
|
+
const INTEGRATION_PROJECT_PATH = __dirname + '/..';
|
|
16
|
+
const ENV_TEST_FILE = path.join(INTEGRATION_PROJECT_PATH, '.env.test.local');
|
|
17
|
+
|
|
18
|
+
async function setupTestApiKey() {
|
|
19
|
+
console.log('š Setting up test API key for integration tests...\n');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Step 1: Check if Laravel project exists
|
|
23
|
+
console.log('š Checking Laravel project...');
|
|
24
|
+
if (!fs.existsSync(path.join(LARAVEL_PROJECT_PATH, 'artisan'))) {
|
|
25
|
+
throw new Error(`Laravel project not found at ${LARAVEL_PROJECT_PATH}`);
|
|
26
|
+
}
|
|
27
|
+
console.log('ā
Laravel project found\n');
|
|
28
|
+
|
|
29
|
+
// Step 2: Generate or get existing API token
|
|
30
|
+
console.log('š Generating API token...');
|
|
31
|
+
const command = `cd ${LARAVEL_PROJECT_PATH} && php artisan tinker --execute="
|
|
32
|
+
\\$user = App\\\\Models\\\\User::firstOrCreate(
|
|
33
|
+
['email' => 'test-integration@more-apartments.test'],
|
|
34
|
+
['name' => 'Integration Test User', 'password' => bcrypt('password')]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Remove old test tokens
|
|
38
|
+
\\$user->tokens()->where('name', 'integration-test')->delete();
|
|
39
|
+
|
|
40
|
+
// Create new token
|
|
41
|
+
\\$token = \\$user->createToken('integration-test', ['*']);
|
|
42
|
+
echo 'TOKEN:' . \\$token->plainTextToken;
|
|
43
|
+
"`;
|
|
44
|
+
|
|
45
|
+
const output = execSync(command, { encoding: 'utf8' });
|
|
46
|
+
|
|
47
|
+
// Extract token from output
|
|
48
|
+
const tokenMatch = output.match(/TOKEN:([a-zA-Z0-9|_-]+)/);
|
|
49
|
+
if (!tokenMatch) {
|
|
50
|
+
throw new Error('Failed to extract API token from Laravel output');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const apiToken = tokenMatch[1];
|
|
54
|
+
console.log('ā
API token generated successfully\n');
|
|
55
|
+
|
|
56
|
+
// Step 3: Update .env.test.local file
|
|
57
|
+
console.log('š Updating test environment configuration...');
|
|
58
|
+
const envContent = `# Auto-generated test environment configuration
|
|
59
|
+
# Generated on: ${new Date().toISOString()}
|
|
60
|
+
|
|
61
|
+
# Laravel API Configuration
|
|
62
|
+
MORE_APARTMENTS_BASE_URL=http://prj-more-apartments.test
|
|
63
|
+
MORE_APARTMENTS_API_KEY=${apiToken}
|
|
64
|
+
MORE_APARTMENTS_INSTANCE=cheap-gotham-apartments
|
|
65
|
+
|
|
66
|
+
# Test Configuration
|
|
67
|
+
NODE_ENV=test
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(ENV_TEST_FILE, envContent);
|
|
71
|
+
console.log('ā
Environment configuration updated\n');
|
|
72
|
+
|
|
73
|
+
// Step 4: Verify API connectivity
|
|
74
|
+
console.log('š Testing API connectivity...');
|
|
75
|
+
try {
|
|
76
|
+
const testCommand = `cd ${INTEGRATION_PROJECT_PATH} && bun run test:integration -- --reporter=dot`;
|
|
77
|
+
execSync(testCommand, { encoding: 'utf8', stdio: 'pipe' });
|
|
78
|
+
console.log('ā
Integration tests can now access the API\n');
|
|
79
|
+
} catch (testError) {
|
|
80
|
+
// Test might fail for other reasons, but if it's not auth, that's OK
|
|
81
|
+
if (testError.stdout && testError.stdout.includes('Unauthenticated')) {
|
|
82
|
+
throw new Error('API token is not working correctly');
|
|
83
|
+
}
|
|
84
|
+
console.log('ā
API authentication is working (tests may have other issues)\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Step 5: Success message
|
|
88
|
+
console.log('š Setup complete!');
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log('Your integration tests are now configured with:');
|
|
91
|
+
console.log(` API Key: ${apiToken.substring(0, 20)}...`);
|
|
92
|
+
console.log(` Base URL: http://prj-more-apartments.test`);
|
|
93
|
+
console.log(` Instance: cheap-gotham-apartments`);
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log('Run integration tests with:');
|
|
96
|
+
console.log(' bun run test:integration');
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('ā Setup failed:', error.message);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Troubleshooting:');
|
|
103
|
+
console.log('1. Make sure Laravel is running at http://prj-more-apartments.test');
|
|
104
|
+
console.log('2. Ensure you can access the Laravel project directory');
|
|
105
|
+
console.log('3. Try running: cd /Users/shelfwood/Projects/prj-more-apartments && php artisan --version');
|
|
106
|
+
console.log('');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Run the setup
|
|
112
|
+
setupTestApiKey();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate generated Zod schemas
|
|
5
|
+
* Ensures schemas are syntactically valid and can be instantiated
|
|
6
|
+
*
|
|
7
|
+
* This script is run by CI/CD to catch invalid schema generation
|
|
8
|
+
* before code is committed or published.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import { resolve, dirname } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const SCHEMAS_FILE = resolve(__dirname, '../src/types/generated/schemas.ts');
|
|
19
|
+
|
|
20
|
+
async function validateSchemas() {
|
|
21
|
+
console.log('š Validating generated Zod schemas...\n');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// 1. Check file exists and has content
|
|
25
|
+
const content = readFileSync(SCHEMAS_FILE, 'utf-8');
|
|
26
|
+
|
|
27
|
+
if (content.length < 100) {
|
|
28
|
+
console.error('ā Schemas file is too small (< 100 bytes).');
|
|
29
|
+
console.error(' Generation may have failed or produced empty output.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`ā
File size: ${content.length} bytes`);
|
|
34
|
+
|
|
35
|
+
// 2. Import and validate each schema
|
|
36
|
+
console.log('š¦ Loading schemas module...');
|
|
37
|
+
const module = await import(SCHEMAS_FILE);
|
|
38
|
+
const schemas = Object.entries(module).filter(([key]) => key.endsWith('Schema'));
|
|
39
|
+
|
|
40
|
+
if (schemas.length === 0) {
|
|
41
|
+
console.error('ā No schemas found in generated file!');
|
|
42
|
+
console.error(' Expected exports ending with "Schema"');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`\nš Found ${schemas.length} schemas to validate:\n`);
|
|
47
|
+
|
|
48
|
+
let failures = 0;
|
|
49
|
+
let warnings = 0;
|
|
50
|
+
|
|
51
|
+
for (const [name, schema] of schemas) {
|
|
52
|
+
try {
|
|
53
|
+
// Test that schema is a valid Zod object
|
|
54
|
+
if (!schema || typeof schema !== 'object') {
|
|
55
|
+
console.error(`ā ${name} - Not a valid object`);
|
|
56
|
+
failures++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for Zod-specific methods
|
|
61
|
+
if (typeof schema.safeParse !== 'function') {
|
|
62
|
+
console.error(`ā ${name} - Missing safeParse() method (not a Zod schema)`);
|
|
63
|
+
failures++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Test that schema can parse (validates structure)
|
|
68
|
+
const result = schema.safeParse({});
|
|
69
|
+
|
|
70
|
+
if (result.success) {
|
|
71
|
+
// Empty object is valid (all fields optional)
|
|
72
|
+
console.log(`ā
${name} - Valid (all fields optional)`);
|
|
73
|
+
} else if (result.error) {
|
|
74
|
+
// Schema has required fields (expected)
|
|
75
|
+
console.log(`ā
${name} - Valid (has required fields)`);
|
|
76
|
+
} else {
|
|
77
|
+
console.warn(`ā ļø ${name} - Unexpected parse result`);
|
|
78
|
+
warnings++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`ā ${name} - Error: ${error.message}`);
|
|
83
|
+
failures++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
88
|
+
console.log(`Total schemas: ${schemas.length}`);
|
|
89
|
+
console.log(`Valid: ${schemas.length - failures}`);
|
|
90
|
+
if (warnings > 0) console.log(`Warnings: ${warnings}`);
|
|
91
|
+
if (failures > 0) console.log(`Failures: ${failures}`);
|
|
92
|
+
console.log('='.repeat(60));
|
|
93
|
+
|
|
94
|
+
if (failures > 0) {
|
|
95
|
+
console.error(`\nā Validation failed: ${failures} invalid schema(s)`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (warnings > 0) {
|
|
100
|
+
console.warn(`\nā ļø Validation completed with ${warnings} warning(s)`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`\nā
All ${schemas.length} schemas validated successfully!`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.code === 'ENOENT') {
|
|
107
|
+
console.error('ā Schemas file not found:', SCHEMAS_FILE);
|
|
108
|
+
console.error(' Run "bun run generate:schemas" first');
|
|
109
|
+
} else if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
110
|
+
console.error('ā Failed to import schemas:', error.message);
|
|
111
|
+
console.error(' Generated file may have syntax errors');
|
|
112
|
+
} else {
|
|
113
|
+
console.error('ā Validation error:', error.message);
|
|
114
|
+
}
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Run validation
|
|
120
|
+
validateSchemas().catch(error => {
|
|
121
|
+
console.error('\nā Fatal validation error:', error);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|