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 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": "1.5.10",
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:skip-export": "node scripts/generate-types.mjs --skip-export",
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
+ });