more-apartments-astro-integration 1.5.10 → 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/README.md +92 -0
- package/dist/.metadata_never_index +0 -0
- package/package.json +7 -3
- 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/README.md
CHANGED
|
@@ -24,6 +24,98 @@ yarn add @shelfwood/more-apartments-astro-integration
|
|
|
24
24
|
bun add @shelfwood/more-apartments-astro-integration
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## Build Process & Type Generation
|
|
28
|
+
|
|
29
|
+
### How Types are Generated
|
|
30
|
+
|
|
31
|
+
This package provides fully-typed API clients generated from the Laravel backend's OpenAPI specification. The types are **not committed to git** and are instead generated during the build process.
|
|
32
|
+
|
|
33
|
+
#### CI/CD Build Process (Automated)
|
|
34
|
+
|
|
35
|
+
When you install or publish this package, the build automatically:
|
|
36
|
+
|
|
37
|
+
1. **Fetches OpenAPI Spec** from GitHub Releases (latest **stable/production** spec from Laravel backend)
|
|
38
|
+
2. **Generates TypeScript Types** using `openapi-typescript`
|
|
39
|
+
3. **Generates Zod Schemas** for runtime validation
|
|
40
|
+
4. **Builds Package** with all generated types included
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# This happens automatically during:
|
|
44
|
+
npm install @shelfwood/more-apartments-astro-integration
|
|
45
|
+
|
|
46
|
+
# Or when publishing:
|
|
47
|
+
npm publish # Runs prepublishOnly hook
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Environment-Specific Fetching:**
|
|
51
|
+
|
|
52
|
+
The package supports fetching from different release channels:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Production (stable releases only - default)
|
|
56
|
+
bun run fetch:spec
|
|
57
|
+
|
|
58
|
+
# Staging (pre-release/staging builds)
|
|
59
|
+
bun run fetch:spec:staging
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**How it works:**
|
|
63
|
+
- **Production:** Fetches from `releases/latest` (stable releases only, excludes pre-releases)
|
|
64
|
+
- **Staging:** Fetches from latest pre-release tagged with `staging-*`
|
|
65
|
+
|
|
66
|
+
#### Local Development Workflow
|
|
67
|
+
|
|
68
|
+
For local development when you have the Laravel project available:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Clone both repositories side-by-side:
|
|
72
|
+
# ~/projects/prj-more-apartments
|
|
73
|
+
# ~/projects/more-apartments-astro-integration
|
|
74
|
+
|
|
75
|
+
cd more-apartments-astro-integration
|
|
76
|
+
|
|
77
|
+
# Generate types from local Laravel project
|
|
78
|
+
bun run generate:types
|
|
79
|
+
|
|
80
|
+
# Or use watch mode for automatic regeneration
|
|
81
|
+
bun run dev:local
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Custom Laravel project path:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# If your Laravel project is elsewhere
|
|
88
|
+
MORE_APARTMENTS_PROJECT_PATH=~/custom/path/to/laravel bun run generate:types
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Manual Type Generation (Advanced)
|
|
92
|
+
|
|
93
|
+
You can also manually fetch and generate types:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# 1. Fetch latest OpenAPI spec from GitHub Releases
|
|
97
|
+
bun run fetch:spec
|
|
98
|
+
|
|
99
|
+
# 2. Generate TypeScript types from fetched spec
|
|
100
|
+
bun run generate:types:local
|
|
101
|
+
|
|
102
|
+
# 3. Generate Zod validation schemas
|
|
103
|
+
bun run generate:schemas
|
|
104
|
+
|
|
105
|
+
# 4. Build package
|
|
106
|
+
bun run build
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Generated Files (Gitignored)
|
|
110
|
+
|
|
111
|
+
The following files are generated during build and **excluded from git**:
|
|
112
|
+
|
|
113
|
+
- `.openapi/api-v1.json` - OpenAPI specification (fetched from GitHub Releases)
|
|
114
|
+
- `src/types/generated/api.d.ts` - TypeScript type definitions
|
|
115
|
+
- `src/types/generated/schemas.ts` - Zod validation schemas
|
|
116
|
+
|
|
117
|
+
These files are automatically generated, so you'll never see them in pull requests or git diffs.
|
|
118
|
+
|
|
27
119
|
## CLI Usage
|
|
28
120
|
|
|
29
121
|
The package includes a powerful CLI for interacting with the More Apartments API directly from the command line.
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "more-apartments-astro-integration",
|
|
3
|
-
"version": "
|
|
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"
|
|
@@ -28,12 +29,15 @@
|
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsup",
|
|
30
31
|
"dev": "tsup --watch",
|
|
31
|
-
"prepublishOnly": "bun run build",
|
|
32
|
+
"prepublishOnly": "bun run fetch:spec && bun run generate:types:local && bun run generate:schemas && bun run build",
|
|
33
|
+
"fetch:spec": "mkdir -p .openapi && gh release download --repo Feelgood-Apartments/prj-more-apartments --pattern 'api-v1.json' --output .openapi/api-v1.json --clobber || (echo '❌ Failed to fetch OpenAPI spec from GitHub Releases (production)' && exit 1)",
|
|
34
|
+
"fetch:spec:staging": "node scripts/fetch-staging-spec.mjs",
|
|
32
35
|
"generate:types": "node scripts/generate-types.mjs",
|
|
33
|
-
"generate:types:
|
|
36
|
+
"generate:types:local": "openapi-typescript .openapi/api-v1.json -o src/types/generated/api.d.ts",
|
|
34
37
|
"generate:schemas": "node scripts/generate-zod-schemas.mjs",
|
|
35
38
|
"validate:schemas": "bun scripts/validate-schemas.mjs",
|
|
36
39
|
"detect:breaking": "node scripts/detect-breaking-changes.mjs",
|
|
40
|
+
"dev:local": "MORE_APARTMENTS_PROJECT_PATH=../prj-more-apartments node scripts/generate-types.mjs && tsup --watch",
|
|
37
41
|
"test": "node test-runner.js all",
|
|
38
42
|
"test:watch": "vitest",
|
|
39
43
|
"test:unit": "node test-runner.js unit",
|
|
@@ -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
|
+
});
|