wanas-zcrm-extractor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/banner.txt +7 -0
- package/bin/cli.js +428 -0
- package/package.json +45 -0
- package/public/app.js +434 -0
- package/public/index.html +255 -0
- package/public/style.css +981 -0
- package/server.js +47 -0
- package/src/controllers/authController.js +213 -0
- package/src/controllers/metadataController.js +317 -0
- package/src/middleware/authMiddleware.js +19 -0
- package/src/routes/authRoutes.js +15 -0
- package/src/routes/metadataRoutes.js +23 -0
- package/src/services/authService.js +273 -0
- package/src/services/metadata/baseMetadataService.js +17 -0
- package/src/services/metadata/creatorMetadataService.js +27 -0
- package/src/services/metadata/crmMetadataService.js +581 -0
- package/src/services/metadata/projectsMetadataService.js +27 -0
- package/src/utils/apiClient.js +82 -0
- package/src/utils/configStore.js +85 -0
- package/src/utils/logger.js +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Zoho CRM V8 Metadata Extractor
|
|
2
|
+
|
|
3
|
+
A robust, production-quality local web application built using **Node.js** and **Express** that authenticates with Zoho CRM using OAuth2 (V8 APIs) and extracts all CRM metadata into structured JSON schemas stored locally.
|
|
4
|
+
|
|
5
|
+
This tool is designed with a clean, modular architecture, enabling future extensions for Zoho Creator and Zoho Projects metadata extraction.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🌟 Key Features
|
|
10
|
+
|
|
11
|
+
1. **OAuth2 Connection Dashboard:** A premium, glassmorphic dark-theme user interface with a real-time system status indicator.
|
|
12
|
+
2. **Server-Sent Events (SSE) Progress Terminal:** Displays live step-by-step extraction logs from the backend directly in a console window.
|
|
13
|
+
3. **Consolidated Data Analysis:** Generates:
|
|
14
|
+
- Module summaries detailing field types, required fields, picklists, formulas, lookup associations, and layout counts.
|
|
15
|
+
- A unified `field_map.json` linking all module fields to their data types.
|
|
16
|
+
- A consolidated `complete_metadata.json` merging all API responses into a single large object.
|
|
17
|
+
4. **Interactive Module Explorer:** Browse, search, filter, and inspect extracted modules, fields, related lists, and raw summaries through an interactive modal viewer.
|
|
18
|
+
5. **Consolidated ZIP Downloads:** Automatically packages all exported data in a single zip archive using native compression.
|
|
19
|
+
6. **Robust Error Handling:**
|
|
20
|
+
- Automatic token refreshes on expiry.
|
|
21
|
+
- Retry mechanism for API rate limits (HTTP 429) using exponential backoff.
|
|
22
|
+
- Detailed failure logging in `storage/logs/errors.log`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📂 Project Structure
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
project/
|
|
30
|
+
├── public/ # Glassmorphic Frontend UI Assets
|
|
31
|
+
│ ├── index.html # Dashboard HTML
|
|
32
|
+
│ ├── style.css # Premium Dark-Theme Styles
|
|
33
|
+
│ └── app.js # SSE Event Listeners & Interactive Explorer logic
|
|
34
|
+
│
|
|
35
|
+
├── src/
|
|
36
|
+
│ ├── controllers/ # Handles Route Requests (OAuth, Extraction, Downloads)
|
|
37
|
+
│ │ ├── authController.js
|
|
38
|
+
│ │ └── metadataController.js
|
|
39
|
+
│ │
|
|
40
|
+
│ ├── middleware/ # Security & Validation Middlewares
|
|
41
|
+
│ │ └── authMiddleware.js
|
|
42
|
+
│ │
|
|
43
|
+
│ ├── routes/ # API Endpoints Routing
|
|
44
|
+
│ │ ├── authRoutes.js
|
|
45
|
+
│ │ └── metadataRoutes.js
|
|
46
|
+
│ │
|
|
47
|
+
│ ├── services/ # Business Logic Layer
|
|
48
|
+
│ │ ├── authService.js # Token Storage, Validation, and Refresh Flow
|
|
49
|
+
│ │ └── metadata/ # Platform-Specific Metadata Extractors
|
|
50
|
+
│ │ ├── baseMetadataService.js # Abstract Base Extractor
|
|
51
|
+
│ │ ├── crmMetadataService.js # CRM V8 Extraction Implementation
|
|
52
|
+
│ │ ├── creatorMetadataService.js # Creator Extractor Stub (Future use)
|
|
53
|
+
│ │ └── projectsMetadataService.js # Projects Extractor Stub (Future use)
|
|
54
|
+
│ │
|
|
55
|
+
│ └── utils/ # Common Utilities
|
|
56
|
+
│ ├── apiClient.js # Axios wrapper with retry, rate-limiting & refresh
|
|
57
|
+
│ └── logger.js # Writes errors/logs to storage/logs/errors.log
|
|
58
|
+
│
|
|
59
|
+
├── storage/ # Local Filesystem Output
|
|
60
|
+
│ ├── auth/
|
|
61
|
+
│ │ └── tokens.json # Saved OAuth2 Access and Refresh Tokens
|
|
62
|
+
│ ├── logs/
|
|
63
|
+
│ │ └── errors.log # API and Process Error Logs
|
|
64
|
+
│ └── metadata/ # Extracted JSON Metadata Files
|
|
65
|
+
│ ├── index.json # Master Index file
|
|
66
|
+
│ ├── modules.json # List of Modules
|
|
67
|
+
│ ├── export/ # Export Files (field_map, complete_metadata, modules)
|
|
68
|
+
│ └── modules/ # Module details, layouts, custom_views, related_lists
|
|
69
|
+
│
|
|
70
|
+
├── .env # Local Environment Configuration variables
|
|
71
|
+
├── package.json # Node.js dependencies
|
|
72
|
+
└── server.js # Application Express Server Entrypoint
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🛠️ Setup & Installation
|
|
78
|
+
|
|
79
|
+
### 1. Prerequisite
|
|
80
|
+
Ensure you have [Node.js](https://nodejs.org/) (v18+ LTS recommended) installed.
|
|
81
|
+
|
|
82
|
+
### 2. Install Dependencies
|
|
83
|
+
Run the following command inside the root folder:
|
|
84
|
+
```bash
|
|
85
|
+
npm install
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Environment Configuration
|
|
89
|
+
Create a `.env` file in the root directory and configure your Zoho client credentials:
|
|
90
|
+
```env
|
|
91
|
+
PORT=3000
|
|
92
|
+
ZOHO_CLIENT_ID=your_client_id_here
|
|
93
|
+
ZOHO_CLIENT_SECRET=your_client_secret_here
|
|
94
|
+
ZOHO_REDIRECT_URI=http://localhost:3000/zoho/callback
|
|
95
|
+
ZOHO_DC=com
|
|
96
|
+
```
|
|
97
|
+
*Note: Make sure your Client ID and Client Secret match the data center domain configured in `ZOHO_DC` (e.g. `com`, `eu`, `in`, `com.au`).*
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🚀 Running the Application
|
|
102
|
+
|
|
103
|
+
### Development Mode
|
|
104
|
+
Runs the server using `nodemon` to automatically restart on code changes:
|
|
105
|
+
```bash
|
|
106
|
+
npm run dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Production Mode
|
|
110
|
+
Runs the server normally:
|
|
111
|
+
```bash
|
|
112
|
+
npm start
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Once running, navigate to the local dashboard in your browser:
|
|
116
|
+
**[http://localhost:3000](http://localhost:3000)**
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🔄 How the Authentication & Extraction Works
|
|
121
|
+
|
|
122
|
+
1. **Authentication:**
|
|
123
|
+
- Click **Login To Zoho** on the dashboard. You will be redirected to Zoho's secure OAuth2 consent screen.
|
|
124
|
+
- Upon granting permissions, Zoho redirects back to `http://localhost:3000/zoho/callback`.
|
|
125
|
+
- The server exchanges the authorization code for `access_token` and `refresh_token`, storing them securely in `storage/auth/tokens.json`.
|
|
126
|
+
|
|
127
|
+
2. **Metadata Extraction:**
|
|
128
|
+
- Click **Extract Metadata** to fire the extraction task.
|
|
129
|
+
- The UI connects to `/extract?stream=true` using standard Server-Sent Events, opening a live logging terminal showing the crawler step-by-step.
|
|
130
|
+
- The service walks through all modules, queries fields, layouts, related lists, custom views, webhooks, and custom functions.
|
|
131
|
+
- Summary files are written sequentially on the disk.
|
|
132
|
+
- The master index and consolidated export files are compiled once the extraction completes.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🔧 Extensibility (Zoho Creator & Zoho Projects)
|
|
137
|
+
|
|
138
|
+
To extend the extractor to Zoho Creator or Zoho Projects, simply:
|
|
139
|
+
1. Implement the extraction pipeline inside `src/services/metadata/creatorMetadataService.js` or `projectsMetadataService.js` which already inherit from the abstract `BaseMetadataService` template.
|
|
140
|
+
2. Update the target selector inside `public/index.html` to enable the platform option.
|
package/banner.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
___
|
|
2
|
+
\ \ __ __ _
|
|
3
|
+
___ \ \ \ \ / /_ _ _ __ __ _ ___ / \ _ __ _ __ ___
|
|
4
|
+
/ / / / \ \ /\ / / _` | '_ \ / _` / __| / _ \ | '_ \| '_ \/ __|
|
|
5
|
+
/ / /__/ \ V V / (_| | | | | (_| \__ \ / ___ \| |_) | |_) \__ \
|
|
6
|
+
\ \ \_/\_/ \__,_|_| |_|\__,_|___/ /_/ \_\ .__/| .__/|___/
|
|
7
|
+
\__\ |_| |_|
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Enable CLI Mode configuration before loading services
|
|
4
|
+
process.env.ZCRM_CLI_MODE = 'true';
|
|
5
|
+
|
|
6
|
+
const { program } = require('commander');
|
|
7
|
+
const inquirer = require('inquirer').default || require('inquirer');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const url = require('url');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { exec } = require('child_process');
|
|
12
|
+
const configStore = require('../src/utils/configStore');
|
|
13
|
+
const authService = require('../src/services/authService');
|
|
14
|
+
const crmMetadataService = require('../src/services/metadata/crmMetadataService');
|
|
15
|
+
|
|
16
|
+
// Default client credentials for generating Zoho login links
|
|
17
|
+
const DEFAULT_CLIENT_ID = '1000.M1XE1M7CA4VFDDIRANB8E5AKIBQ72U';
|
|
18
|
+
const DEFAULT_CLIENT_SECRET = 'baae847487d4d1fc762214bad5aeb272dd02f6f991';
|
|
19
|
+
|
|
20
|
+
// Load and color the ASCII banner
|
|
21
|
+
const getBanner = () => {
|
|
22
|
+
try {
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const bannerPath = path.join(__dirname, '../banner.txt');
|
|
25
|
+
if (fs.existsSync(bannerPath)) {
|
|
26
|
+
const banner = fs.readFileSync(bannerPath, 'utf8');
|
|
27
|
+
return `\x1b[34m${banner}\x1b[0m\n`;
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Fail silently
|
|
31
|
+
}
|
|
32
|
+
return '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Draws a beautiful text progress bar in the console.
|
|
37
|
+
* @param {number} current - Current completed index
|
|
38
|
+
* @param {number} total - Total items to complete
|
|
39
|
+
* @param {string} label - The phase label (e.g., 'Modules', 'Functions')
|
|
40
|
+
* @param {string} action - The currently executing specific item
|
|
41
|
+
*/
|
|
42
|
+
function drawProgress(current, total, label, action) {
|
|
43
|
+
const readline = require('readline');
|
|
44
|
+
const width = 30;
|
|
45
|
+
const percent = total > 0 ? Math.min(100, Math.max(0, Math.round((current / total) * 100))) : 0;
|
|
46
|
+
const filled = Math.round((percent / 100) * width);
|
|
47
|
+
const empty = width - filled;
|
|
48
|
+
|
|
49
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
50
|
+
const truncatedAction = action.length > 40 ? action.substring(0, 37) + '...' : action;
|
|
51
|
+
|
|
52
|
+
// Clear the current line and move cursor to start
|
|
53
|
+
readline.clearLine(process.stdout, 0);
|
|
54
|
+
readline.cursorTo(process.stdout, 0);
|
|
55
|
+
|
|
56
|
+
// Print progress bar, percentage, and label in beautiful colors
|
|
57
|
+
process.stdout.write(`\x1b[34m[${bar}]\x1b[0m \x1b[1;32m${percent}%\x1b[0m | \x1b[1m${label}\x1b[0m: ${truncatedAction}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Version and metadata configuration
|
|
61
|
+
program
|
|
62
|
+
.name('zcrm')
|
|
63
|
+
.description('Zoho CRM V8 Metadata Extractor CLI Utility')
|
|
64
|
+
.version('1.0.0')
|
|
65
|
+
.addHelpText('before', getBanner());
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CLI Callback Server validation & token retrieval flow.
|
|
69
|
+
* Spins up a temporary server to process Zoho redirect callback.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} clientId - Zoho Client ID.
|
|
72
|
+
* @param {string} clientSecret - Zoho Client Secret.
|
|
73
|
+
* @param {string} redirectUri - Zoho Redirect URI (configured in developer console).
|
|
74
|
+
* @param {string} dc - Zoho DC domain.
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
function runOAuthServer(clientId, clientSecret, redirectUri, dc) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
let parsedUrl;
|
|
80
|
+
try {
|
|
81
|
+
parsedUrl = new URL(redirectUri);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return reject(new Error(`Invalid redirect URI structure: ${redirectUri}`));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const port = parsedUrl.port || 80;
|
|
87
|
+
|
|
88
|
+
const server = http.createServer(async (req, res) => {
|
|
89
|
+
const parsedReqUrl = url.parse(req.url, true);
|
|
90
|
+
|
|
91
|
+
// Handle the redirect callback path
|
|
92
|
+
if (parsedReqUrl.pathname === parsedUrl.pathname) {
|
|
93
|
+
const code = parsedReqUrl.query.code;
|
|
94
|
+
const error = parsedReqUrl.query.error;
|
|
95
|
+
|
|
96
|
+
if (error) {
|
|
97
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
98
|
+
res.end(`
|
|
99
|
+
<html>
|
|
100
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding-top: 50px; background-color: #0b0f19; color: #f3f4f6;">
|
|
101
|
+
<h1 style="color: #ef4444;">Authorization Failed</h1>
|
|
102
|
+
<p>${error}</p>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
`);
|
|
106
|
+
server.close();
|
|
107
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (code) {
|
|
112
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
113
|
+
res.end(`
|
|
114
|
+
<html>
|
|
115
|
+
<body style="font-family: Arial, sans-serif; text-align: center; padding-top: 50px; background-color: #0b0f19; color: #f3f4f6;">
|
|
116
|
+
<h1 style="color: #10b981;">Authentication Successful!</h1>
|
|
117
|
+
<p>You may now close this browser window and return to your terminal.</p>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
console.log('\nCode received. Fetching tokens from Zoho accounts server...');
|
|
124
|
+
|
|
125
|
+
// Save client credentials to configStore so handleCallback has them
|
|
126
|
+
configStore.save({
|
|
127
|
+
client_id: clientId,
|
|
128
|
+
client_secret: clientSecret,
|
|
129
|
+
redirect_uri: redirectUri,
|
|
130
|
+
dc: dc
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await authService.handleCallback(code);
|
|
134
|
+
server.close();
|
|
135
|
+
resolve();
|
|
136
|
+
} catch (exchangeErr) {
|
|
137
|
+
server.close();
|
|
138
|
+
reject(exchangeErr);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
142
|
+
res.end('Missing code parameter.');
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
res.writeHead(404);
|
|
146
|
+
res.end();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
server.on('error', (err) => {
|
|
151
|
+
if (err.code === 'EADDRINUSE') {
|
|
152
|
+
reject(new Error(`Port ${port} is currently in use. Please stop other servers or choose another port.`));
|
|
153
|
+
} else {
|
|
154
|
+
reject(err);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
server.listen(port, () => {
|
|
159
|
+
console.log(`Temporary listener started on port ${port}...`);
|
|
160
|
+
|
|
161
|
+
// Initialize config details for authorizer URL generation
|
|
162
|
+
configStore.save({
|
|
163
|
+
client_id: clientId,
|
|
164
|
+
client_secret: clientSecret,
|
|
165
|
+
redirect_uri: redirectUri,
|
|
166
|
+
dc: dc
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const authUrl = authService.getAuthorizationUrl();
|
|
170
|
+
|
|
171
|
+
console.log('\n========================================================================');
|
|
172
|
+
console.log('Please complete the authentication process in your browser.');
|
|
173
|
+
console.log('If the browser does not open automatically, visit this URL:');
|
|
174
|
+
console.log(authUrl);
|
|
175
|
+
console.log('========================================================================\n');
|
|
176
|
+
|
|
177
|
+
// Attempt to auto-open in default browser
|
|
178
|
+
const launch = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
179
|
+
exec(`${launch} "${authUrl.replace(/"/g, '\\"')}"`, (execErr) => {
|
|
180
|
+
// Suppress errors (user can manually click printed link if this fails)
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// REGISTER COMMAND: login
|
|
187
|
+
program
|
|
188
|
+
.command('login')
|
|
189
|
+
.description('Login to Zoho CRM using OAuth2 credentials')
|
|
190
|
+
.option('--client-id <id>', 'Zoho Client ID')
|
|
191
|
+
.option('--client-secret <secret>', 'Zoho Client Secret')
|
|
192
|
+
.option('--dc <dc>', 'Zoho Data Center (com, eu, in, jp, com.au, com.cn)')
|
|
193
|
+
.option('--redirect-uri <uri>', 'OAuth Redirect Callback URI')
|
|
194
|
+
.option('--refresh-token <token>', 'Headless Refresh Token bypass')
|
|
195
|
+
.action(async (options) => {
|
|
196
|
+
try {
|
|
197
|
+
let clientId = options.clientId || DEFAULT_CLIENT_ID;
|
|
198
|
+
let clientSecret = options.clientSecret || DEFAULT_CLIENT_SECRET;
|
|
199
|
+
let dc = options.dc;
|
|
200
|
+
let redirectUri = options.redirectUri;
|
|
201
|
+
const refreshToken = options.refreshToken;
|
|
202
|
+
|
|
203
|
+
// Run interactive prompts if parameters are missing
|
|
204
|
+
const prompts = [];
|
|
205
|
+
if (!dc) {
|
|
206
|
+
prompts.push({
|
|
207
|
+
type: 'select',
|
|
208
|
+
name: 'dc',
|
|
209
|
+
message: 'Select Zoho Data Center (DC):',
|
|
210
|
+
choices: ['com', 'eu', 'in', 'jp', 'com.au', 'com.cn'],
|
|
211
|
+
default: 'com'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (!redirectUri && !refreshToken) {
|
|
215
|
+
prompts.push({
|
|
216
|
+
type: 'input',
|
|
217
|
+
name: 'redirectUri',
|
|
218
|
+
message: 'Enter your Redirect URI:',
|
|
219
|
+
default: 'http://localhost:3000/zoho/callback'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (prompts.length > 0) {
|
|
224
|
+
const answers = await inquirer.prompt(prompts);
|
|
225
|
+
dc = dc || answers.dc;
|
|
226
|
+
redirectUri = redirectUri || answers.redirectUri;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 1. Headless flow using direct refresh token
|
|
230
|
+
if (refreshToken) {
|
|
231
|
+
console.log('Bypassing browser callback. Verifying refresh token credentials...');
|
|
232
|
+
|
|
233
|
+
configStore.save({
|
|
234
|
+
client_id: clientId,
|
|
235
|
+
client_secret: clientSecret,
|
|
236
|
+
dc: dc || 'com',
|
|
237
|
+
redirect_uri: redirectUri || 'http://localhost:3000/zoho/callback',
|
|
238
|
+
tokens: {
|
|
239
|
+
refresh_token: refreshToken
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Initialize and refresh to verify key validity
|
|
244
|
+
authService.loadTokens();
|
|
245
|
+
await authService.refreshAccessToken();
|
|
246
|
+
|
|
247
|
+
console.log('🎉 Credentials and session authenticated successfully via Refresh Token!');
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 2. Interactive Browser OAuth2 flow
|
|
252
|
+
await runOAuthServer(clientId, clientSecret, redirectUri, dc);
|
|
253
|
+
console.log('🎉 Successfully logged in! Access configuration saved.');
|
|
254
|
+
process.exit(0);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error('\n❌ Login error:', err.message);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// REGISTER COMMAND: logout
|
|
262
|
+
program
|
|
263
|
+
.command('logout')
|
|
264
|
+
.description('Logout of Zoho CRM and clear stored configurations')
|
|
265
|
+
.action(() => {
|
|
266
|
+
configStore.clear();
|
|
267
|
+
console.log('🎉 Stored credentials cleared successfully.');
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// REGISTER COMMAND: pull
|
|
272
|
+
program
|
|
273
|
+
.command('pull')
|
|
274
|
+
.description('Pull Zoho CRM metadata and save as JSON file directory structure')
|
|
275
|
+
.option('-o, --output <dir>', 'Output folder path', './metadata')
|
|
276
|
+
.action(async (options) => {
|
|
277
|
+
console.log(getBanner());
|
|
278
|
+
// Reload tokens
|
|
279
|
+
authService.loadTokens();
|
|
280
|
+
|
|
281
|
+
if (!authService.isAuthenticated()) {
|
|
282
|
+
console.error('\n❌ Error: Not authenticated. Please login first using:\n zcrm login');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const outputDir = path.resolve(options.output);
|
|
287
|
+
console.log(`\x1b[36mInitializing CRM Metadata Pulling...\x1b[0m`);
|
|
288
|
+
console.log(`Destination Folder: ${outputDir}\n`);
|
|
289
|
+
|
|
290
|
+
let currentModule = 0;
|
|
291
|
+
let totalModules = 0;
|
|
292
|
+
let currentFunction = 0;
|
|
293
|
+
let totalFunctions = 0;
|
|
294
|
+
let inModulesPhase = false;
|
|
295
|
+
let inFunctionsPhase = false;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await crmMetadataService.extract((stage, message, details = {}) => {
|
|
299
|
+
switch (stage) {
|
|
300
|
+
case 'FETCH_ORG':
|
|
301
|
+
console.log(`\x1b[34mℹ\x1b[0m ${message}`);
|
|
302
|
+
break;
|
|
303
|
+
case 'FETCH_ORG_SUCCESS':
|
|
304
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
305
|
+
break;
|
|
306
|
+
case 'FETCH_MODULES':
|
|
307
|
+
console.log(`\x1b[34mℹ\x1b[0m ${message}`);
|
|
308
|
+
break;
|
|
309
|
+
case 'FETCH_MODULES_SUCCESS':
|
|
310
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
311
|
+
break;
|
|
312
|
+
case 'MODULE_START':
|
|
313
|
+
if (!inModulesPhase) {
|
|
314
|
+
console.log(`\n\x1b[1mExtracting Modules Metadata:\x1b[0m`);
|
|
315
|
+
inModulesPhase = true;
|
|
316
|
+
}
|
|
317
|
+
currentModule = details.index;
|
|
318
|
+
totalModules = details.total;
|
|
319
|
+
drawProgress(currentModule - 1, totalModules, 'Modules', `Extracting ${details.module}...`);
|
|
320
|
+
break;
|
|
321
|
+
case 'MODULE_SUCCESS':
|
|
322
|
+
if (currentModule === totalModules) {
|
|
323
|
+
const readline = require('readline');
|
|
324
|
+
readline.clearLine(process.stdout, 0);
|
|
325
|
+
readline.cursorTo(process.stdout, 0);
|
|
326
|
+
console.log(`\x1b[32m✔\x1b[0m All modules metadata extracted successfully.`);
|
|
327
|
+
inModulesPhase = false;
|
|
328
|
+
} else {
|
|
329
|
+
drawProgress(currentModule, totalModules, 'Modules', `Completed ${details.module || ''}`);
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
case 'FETCH_WEBHOOKS':
|
|
333
|
+
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
334
|
+
break;
|
|
335
|
+
case 'FETCH_WEBHOOKS_SUCCESS':
|
|
336
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
337
|
+
break;
|
|
338
|
+
case 'FETCH_PROFILES':
|
|
339
|
+
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
340
|
+
break;
|
|
341
|
+
case 'FETCH_PROFILES_SUCCESS':
|
|
342
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
343
|
+
break;
|
|
344
|
+
case 'FETCH_ROLES':
|
|
345
|
+
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
346
|
+
break;
|
|
347
|
+
case 'FETCH_ROLES_SUCCESS':
|
|
348
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
349
|
+
break;
|
|
350
|
+
case 'FETCH_FUNCTIONS':
|
|
351
|
+
console.log(`\n\x1b[34mℹ\x1b[0m ${message}`);
|
|
352
|
+
break;
|
|
353
|
+
case 'FETCH_FUNCTIONS_SUCCESS':
|
|
354
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
355
|
+
break;
|
|
356
|
+
case 'FUNCTION_START':
|
|
357
|
+
if (!inFunctionsPhase) {
|
|
358
|
+
console.log(`\n\x1b[1mExtracting Custom Functions Deluge Scripts:\x1b[0m`);
|
|
359
|
+
inFunctionsPhase = true;
|
|
360
|
+
}
|
|
361
|
+
currentFunction = details.index;
|
|
362
|
+
totalFunctions = details.total;
|
|
363
|
+
drawProgress(currentFunction - 1, totalFunctions, 'Functions', `Downloading ${details.name}...`);
|
|
364
|
+
break;
|
|
365
|
+
case 'FUNCTION_SUCCESS':
|
|
366
|
+
case 'FUNCTION_WARNING':
|
|
367
|
+
if (currentFunction === totalFunctions) {
|
|
368
|
+
const readline = require('readline');
|
|
369
|
+
readline.clearLine(process.stdout, 0);
|
|
370
|
+
readline.cursorTo(process.stdout, 0);
|
|
371
|
+
console.log(`\x1b[32m✔\x1b[0m All custom functions deluge scripts extracted.`);
|
|
372
|
+
inFunctionsPhase = false;
|
|
373
|
+
} else {
|
|
374
|
+
drawProgress(currentFunction, totalFunctions, 'Functions', `Completed ${details.name || ''}`);
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
case 'FAILURE':
|
|
378
|
+
if (inModulesPhase || inFunctionsPhase) {
|
|
379
|
+
process.stdout.write('\n');
|
|
380
|
+
}
|
|
381
|
+
console.error(`\n❌ Error: ${message}`);
|
|
382
|
+
break;
|
|
383
|
+
case 'COMPLETE':
|
|
384
|
+
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
385
|
+
console.log('🎉 \x1b[1;32mZoho CRM Metadata Pull Completed Successfully!\x1b[0m');
|
|
386
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
387
|
+
console.log(` \x1b[1mOrganization:\x1b[0m \x1b[36m${details.crm_org || 'Unknown Org'}\x1b[0m`);
|
|
388
|
+
console.log(` \x1b[1mModules Processed:\x1b[0m \x1b[32m${details.modulesCount || 0}\x1b[0m`);
|
|
389
|
+
console.log(` \x1b[1mTotal Fields:\x1b[0m \x1b[32m${details.totalFields || 0}\x1b[0m`);
|
|
390
|
+
console.log(` \x1b[1mTotal Layouts:\x1b[0m \x1b[32m${details.totalLayouts || 0}\x1b[0m`);
|
|
391
|
+
console.log(` \x1b[1mRelated Lists:\x1b[0m \x1b[35m${details.totalRelatedLists || 0}\x1b[0m (saved to extra/)`);
|
|
392
|
+
console.log(` \x1b[1mCustom Views:\x1b[0m \x1b[35m${details.totalCustomViews || 0}\x1b[0m (saved to extra/)`);
|
|
393
|
+
console.log(` \x1b[1mOutput Directory:\x1b[0m \x1b[34m${outputDir}\x1b[0m`);
|
|
394
|
+
|
|
395
|
+
if (details.errors && details.errors.length > 0) {
|
|
396
|
+
console.log(`\n \x1b[1;33m⚠️ Warnings:\x1b[0m \x1b[33m${details.errors.length} API request(s) failed or were skipped due to permissions.\x1b[0m`);
|
|
397
|
+
}
|
|
398
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
399
|
+
break;
|
|
400
|
+
default:
|
|
401
|
+
if (message && !message.startsWith(' ->')) {
|
|
402
|
+
console.log(`\x1b[34mℹ\x1b[0m ${message}`);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}, outputDir);
|
|
407
|
+
|
|
408
|
+
process.exit(0);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error(`\n❌ Metadata pull failed: ${error.message}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// REGISTER COMMAND: dashboard
|
|
416
|
+
program
|
|
417
|
+
.command('dashboard')
|
|
418
|
+
.description('Start the local Zoho CRM Extractor web dashboard')
|
|
419
|
+
.option('-p, --port <port>', 'Port to run the dashboard on', '3000')
|
|
420
|
+
.action((options) => {
|
|
421
|
+
process.env.PORT = options.port;
|
|
422
|
+
process.env.ZCRM_CLI_MODE = 'false';
|
|
423
|
+
console.log(`\x1b[36mLaunching Extractor Dashboard...\x1b[0m`);
|
|
424
|
+
require('../server.js');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Run parser
|
|
428
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wanas-zcrm-extractor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local Zoho CRM V8 Metadata Extractor CLI and Web Dashboard Utility",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zcrm": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js",
|
|
11
|
+
"dev": "nodemon server.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"public",
|
|
17
|
+
"server.js",
|
|
18
|
+
"banner.txt",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"zoho",
|
|
24
|
+
"crm",
|
|
25
|
+
"metadata",
|
|
26
|
+
"extractor",
|
|
27
|
+
"oauth2",
|
|
28
|
+
"cli",
|
|
29
|
+
"deluge",
|
|
30
|
+
"zdk"
|
|
31
|
+
],
|
|
32
|
+
"author": "Wanas Apps",
|
|
33
|
+
"license": "ISC",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"axios": "^1.7.2",
|
|
36
|
+
"commander": "^15.0.0",
|
|
37
|
+
"dotenv": "^16.4.5",
|
|
38
|
+
"express": "^4.19.2",
|
|
39
|
+
"fs-extra": "^11.2.0",
|
|
40
|
+
"inquirer": "^14.0.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"nodemon": "^3.1.3"
|
|
44
|
+
}
|
|
45
|
+
}
|