@vezlo/assistant-server 1.3.0 ā 2.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 -43
- package/bin/vezlo-server.js +1 -1
- package/database-schema.sql +193 -33
- package/dist/knexfile.d.ts.map +1 -1
- package/dist/knexfile.js +17 -8
- package/dist/knexfile.js.map +1 -1
- package/dist/src/config/database.d.ts.map +1 -1
- package/dist/src/config/database.js +9 -1
- package/dist/src/config/database.js.map +1 -1
- package/dist/src/config/global.d.ts.map +1 -1
- package/dist/src/config/global.js +5 -2
- package/dist/src/config/global.js.map +1 -1
- package/dist/src/config/knex.d.ts.map +1 -1
- package/dist/src/config/knex.js +22 -2
- package/dist/src/config/knex.js.map +1 -1
- package/dist/src/config/swagger.d.ts.map +1 -1
- package/dist/src/config/swagger.js +34 -73
- package/dist/src/config/swagger.js.map +1 -1
- package/dist/src/controllers/ApiKeyController.d.ts +17 -0
- package/dist/src/controllers/ApiKeyController.d.ts.map +1 -0
- package/dist/src/controllers/ApiKeyController.js +84 -0
- package/dist/src/controllers/ApiKeyController.js.map +1 -0
- package/dist/src/controllers/AuthController.d.ts +14 -0
- package/dist/src/controllers/AuthController.d.ts.map +1 -0
- package/dist/src/controllers/AuthController.js +212 -0
- package/dist/src/controllers/AuthController.js.map +1 -0
- package/dist/src/controllers/ChatController.d.ts +8 -5
- package/dist/src/controllers/ChatController.d.ts.map +1 -1
- package/dist/src/controllers/ChatController.js +139 -31
- package/dist/src/controllers/ChatController.js.map +1 -1
- package/dist/src/controllers/KnowledgeController.d.ts +5 -4
- package/dist/src/controllers/KnowledgeController.d.ts.map +1 -1
- package/dist/src/controllers/KnowledgeController.js +54 -16
- package/dist/src/controllers/KnowledgeController.js.map +1 -1
- package/dist/src/middleware/auth.d.ts +51 -0
- package/dist/src/middleware/auth.d.ts.map +1 -0
- package/dist/src/middleware/auth.js +232 -0
- package/dist/src/middleware/auth.js.map +1 -0
- package/dist/src/middleware/errorHandler.d.ts.map +1 -1
- package/dist/src/middleware/errorHandler.js +13 -19
- package/dist/src/middleware/errorHandler.js.map +1 -1
- package/dist/src/migrations/001_initial_schema.d.ts.map +1 -1
- package/dist/src/migrations/001_initial_schema.js +39 -64
- package/dist/src/migrations/001_initial_schema.js.map +1 -1
- package/dist/src/migrations/002_multitenancy_schema.d.ts +4 -0
- package/dist/src/migrations/002_multitenancy_schema.d.ts.map +1 -0
- package/dist/src/migrations/002_multitenancy_schema.js +119 -0
- package/dist/src/migrations/002_multitenancy_schema.js.map +1 -0
- package/dist/src/schemas/AuthSchemas.d.ts +89 -0
- package/dist/src/schemas/AuthSchemas.d.ts.map +1 -0
- package/dist/src/schemas/AuthSchemas.js +63 -0
- package/dist/src/schemas/AuthSchemas.js.map +1 -0
- package/dist/src/schemas/CommonSchemas.d.ts +62 -0
- package/dist/src/schemas/CommonSchemas.d.ts.map +1 -0
- package/dist/src/schemas/CommonSchemas.js +65 -0
- package/dist/src/schemas/CommonSchemas.js.map +1 -0
- package/dist/src/schemas/ConversationSchemas.d.ts +64 -27
- package/dist/src/schemas/ConversationSchemas.d.ts.map +1 -1
- package/dist/src/schemas/ConversationSchemas.js +28 -9
- package/dist/src/schemas/ConversationSchemas.js.map +1 -1
- package/dist/src/schemas/FeedbackSchemas.d.ts +43 -5
- package/dist/src/schemas/FeedbackSchemas.d.ts.map +1 -1
- package/dist/src/schemas/FeedbackSchemas.js +20 -2
- package/dist/src/schemas/FeedbackSchemas.js.map +1 -1
- package/dist/src/schemas/KnowledgeSchemas.d.ts +114 -35
- package/dist/src/schemas/KnowledgeSchemas.d.ts.map +1 -1
- package/dist/src/schemas/KnowledgeSchemas.js +58 -16
- package/dist/src/schemas/KnowledgeSchemas.js.map +1 -1
- package/dist/src/schemas/MessageSchemas.d.ts +57 -8
- package/dist/src/schemas/MessageSchemas.d.ts.map +1 -1
- package/dist/src/schemas/MessageSchemas.js +22 -3
- package/dist/src/schemas/MessageSchemas.js.map +1 -1
- package/dist/src/schemas/index.d.ts +410 -68
- package/dist/src/schemas/index.d.ts.map +1 -1
- package/dist/src/schemas/index.js +8 -2
- package/dist/src/schemas/index.js.map +1 -1
- package/dist/src/server.js +1047 -615
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/AIService.d.ts +1 -2
- package/dist/src/services/AIService.d.ts.map +1 -1
- package/dist/src/services/AIService.js +6 -32
- package/dist/src/services/AIService.js.map +1 -1
- package/dist/src/services/ApiKeyService.d.ts +38 -0
- package/dist/src/services/ApiKeyService.d.ts.map +1 -0
- package/dist/src/services/ApiKeyService.js +123 -0
- package/dist/src/services/ApiKeyService.js.map +1 -0
- package/dist/src/services/KnowledgeBaseService.d.ts +2 -2
- package/dist/src/services/KnowledgeBaseService.d.ts.map +1 -1
- package/dist/src/services/KnowledgeBaseService.js +9 -2
- package/dist/src/services/KnowledgeBaseService.js.map +1 -1
- package/dist/src/services/MigrationService.d.ts +1 -1
- package/dist/src/services/MigrationService.d.ts.map +1 -1
- package/dist/src/services/MigrationService.js +4 -8
- package/dist/src/services/MigrationService.js.map +1 -1
- package/dist/src/services/SetupService.d.ts +102 -0
- package/dist/src/services/SetupService.d.ts.map +1 -0
- package/dist/src/services/SetupService.js +343 -0
- package/dist/src/services/SetupService.js.map +1 -0
- package/dist/src/storage/ConversationRepository.d.ts.map +1 -1
- package/dist/src/storage/ConversationRepository.js +42 -8
- package/dist/src/storage/ConversationRepository.js.map +1 -1
- package/dist/src/storage/MessageRepository.d.ts.map +1 -1
- package/dist/src/storage/MessageRepository.js +23 -27
- package/dist/src/storage/MessageRepository.js.map +1 -1
- package/dist/src/types/index.d.ts +0 -8
- package/dist/src/types/index.d.ts.map +1 -1
- package/env.example +7 -5
- package/knexfile.ts +17 -8
- package/package.json +25 -16
- package/scripts/generate-key.js +124 -0
- package/scripts/seed-default.js +72 -0
- package/scripts/setup.js +410 -149
- package/scripts/validate-db.js +46 -13
package/scripts/setup.js
CHANGED
|
@@ -39,36 +39,18 @@ function question(prompt) {
|
|
|
39
39
|
async function main() {
|
|
40
40
|
console.clear();
|
|
41
41
|
log('\nš Vezlo Assistant Server Setup Wizard\n', 'bright');
|
|
42
|
-
log('This wizard will help you configure your server in
|
|
43
|
-
log(' 1.
|
|
42
|
+
log('This wizard will help you configure your server in 4 easy steps:\n', 'blue');
|
|
43
|
+
log(' 1. Supabase Database Configuration');
|
|
44
44
|
log(' 2. OpenAI API Configuration');
|
|
45
|
-
log(' 3.
|
|
45
|
+
log(' 3. Environment Validation');
|
|
46
|
+
log(' 4. Database Migration Setup\n');
|
|
46
47
|
|
|
47
|
-
// Step 1:
|
|
48
|
+
// Step 1: Supabase Configuration
|
|
48
49
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
49
|
-
log(' STEP 1: Database Configuration', 'bright');
|
|
50
|
+
log(' STEP 1: Supabase Database Configuration', 'bright');
|
|
50
51
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
log(' [1] Supabase (Recommended)');
|
|
54
|
-
log(' [2] PostgreSQL (Direct Connection)');
|
|
55
|
-
log(' [3] Use existing .env file\n');
|
|
56
|
-
|
|
57
|
-
const dbChoice = await question('Enter your choice (1-3):');
|
|
58
|
-
|
|
59
|
-
let config = {};
|
|
60
|
-
|
|
61
|
-
if (dbChoice === '1') {
|
|
62
|
-
config = await setupSupabase();
|
|
63
|
-
} else if (dbChoice === '2') {
|
|
64
|
-
config = await setupPostgreSQL();
|
|
65
|
-
} else if (dbChoice === '3') {
|
|
66
|
-
config = await loadExistingConfig();
|
|
67
|
-
} else {
|
|
68
|
-
log('\nā Invalid choice. Exiting...', 'red');
|
|
69
|
-
rl.close();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
53
|
+
const config = await setupSupabase();
|
|
72
54
|
|
|
73
55
|
// Step 2: OpenAI Configuration
|
|
74
56
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
@@ -81,146 +63,247 @@ async function main() {
|
|
|
81
63
|
const aiModel = await question('AI Model (default: gpt-4o):') || 'gpt-4o';
|
|
82
64
|
config.AI_MODEL = aiModel.trim();
|
|
83
65
|
|
|
66
|
+
const aiTemperature = await question('AI Temperature (default: 0.7):') || '0.7';
|
|
67
|
+
config.AI_TEMPERATURE = aiTemperature.trim();
|
|
68
|
+
|
|
69
|
+
const aiMaxTokens = await question('AI Max Tokens (default: 1000):') || '1000';
|
|
70
|
+
config.AI_MAX_TOKENS = aiMaxTokens.trim();
|
|
71
|
+
|
|
84
72
|
// Step 3: Save Configuration
|
|
85
73
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
86
74
|
log(' STEP 3: Save Configuration', 'bright');
|
|
87
75
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
88
76
|
|
|
89
77
|
const envPath = path.join(process.cwd(), '.env');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
log('Preparing to write environment configuration (.env)...', 'yellow');
|
|
79
|
+
const createdEnv = await saveEnvFile(envPath, config);
|
|
80
|
+
if (createdEnv) {
|
|
81
|
+
log(`ā
Configuration saved to ${envPath}`, 'green');
|
|
82
|
+
} else {
|
|
83
|
+
log('ā¹ļø Using existing .env (no overwrite). Review values as needed.', 'yellow');
|
|
84
|
+
}
|
|
93
85
|
|
|
94
|
-
// Step 4:
|
|
86
|
+
// Step 4: Environment Validation
|
|
95
87
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
96
|
-
log(' STEP 4:
|
|
88
|
+
log(' STEP 4: Environment Validation', 'bright');
|
|
97
89
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
98
90
|
|
|
99
|
-
const
|
|
91
|
+
const validationStatus = await validateEnvironment(config);
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
// Step 5: Database Migration Setup
|
|
94
|
+
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
95
|
+
log(' STEP 5: Database Migration Setup', 'bright');
|
|
96
|
+
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
97
|
+
|
|
98
|
+
const migrationStatus = await setupMigrations(config, validationStatus) || { migrations: validationStatus.database === 'success' ? 'success' : 'skipped' };
|
|
99
|
+
|
|
100
|
+
// Step 6: Default Data Setup (only if migrations succeeded)
|
|
101
|
+
if (migrationStatus.migrations === 'success') {
|
|
102
|
+
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
103
|
+
log(' STEP 6: Default Data Setup', 'bright');
|
|
104
|
+
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
105
|
+
|
|
106
|
+
const defaultDataStatus = await setupDefaultData(config);
|
|
107
|
+
migrationStatus.defaultData = defaultDataStatus;
|
|
108
|
+
|
|
109
|
+
// Step 7: API Key Generation (only if default data setup succeeded)
|
|
110
|
+
if (defaultDataStatus === 'success') {
|
|
111
|
+
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'cyan');
|
|
112
|
+
log(' STEP 7: API Key Generation', 'bright');
|
|
113
|
+
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'cyan');
|
|
114
|
+
|
|
115
|
+
const apiKeyStatus = await setupApiKey(config);
|
|
116
|
+
migrationStatus.apiKey = apiKeyStatus;
|
|
117
|
+
} else {
|
|
118
|
+
migrationStatus.apiKey = 'skipped';
|
|
119
|
+
}
|
|
103
120
|
} else {
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
migrationStatus.defaultData = 'skipped';
|
|
122
|
+
migrationStatus.apiKey = 'skipped';
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
// Final Instructions
|
|
125
|
+
// Final Instructions / Summary
|
|
109
126
|
log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā', 'green');
|
|
110
127
|
log(' š Setup Complete!', 'bright');
|
|
111
128
|
log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n', 'green');
|
|
112
129
|
|
|
113
|
-
|
|
130
|
+
// Summary
|
|
131
|
+
log('Summary:', 'bright');
|
|
132
|
+
const supaStatus = validationStatus.supabaseApi === 'success' ? 'OK' : (validationStatus.supabaseApi === 'failed' ? 'FAILED' : 'UNKNOWN');
|
|
133
|
+
log(` Supabase API: ${supaStatus === 'OK' ? colors.green + 'OK' : supaStatus === 'FAILED' ? colors.red + 'FAILED' : colors.yellow + 'UNKNOWN'}${colors.reset}`);
|
|
134
|
+
log(` Database: ${validationStatus.database === 'success' ? colors.green + 'OK' : colors.red + (validationStatus.database === 'skipped' ? 'SKIPPED' : 'FAILED')}${colors.reset}`);
|
|
135
|
+
log(` Migrations: ${migrationStatus.migrations === 'success' ? colors.green + 'OK' : migrationStatus.migrations === 'skipped' ? colors.yellow + 'SKIPPED' : colors.red + 'FAILED'}${colors.reset}`);
|
|
136
|
+
log(` Default Data: ${migrationStatus.defaultData === 'success' ? colors.green + 'OK' : migrationStatus.defaultData === 'skipped' ? colors.yellow + 'SKIPPED' : colors.red + 'FAILED'}${colors.reset}`);
|
|
137
|
+
log(` API Key: ${migrationStatus.apiKey === 'success' ? colors.green + 'OK' : migrationStatus.apiKey === 'skipped' ? colors.yellow + 'SKIPPED' : colors.red + 'FAILED'}${colors.reset}`);
|
|
138
|
+
|
|
139
|
+
log('\nNext steps:');
|
|
114
140
|
log(' 1. Review your .env file');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
if (migrationStatus.migrations !== 'success') {
|
|
142
|
+
log('\nā ļø IMPORTANT: Migrations were not run. You must run migrations first before seeding default data.', 'yellow');
|
|
143
|
+
log(' 2. Run database migrations: ' + colors.bright + 'npm run migrate:latest' + colors.reset);
|
|
144
|
+
log(' 3. Then run seed: ' + colors.bright + 'npm run seed-default' + colors.reset + ' (only after migrations complete)', 'yellow');
|
|
145
|
+
log(' 4. Generate API key: ' + colors.bright + 'npm run generate-key' + colors.reset + ' (if not already done)');
|
|
146
|
+
log(' 5. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
147
|
+
log(' 6. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
148
|
+
log(' 7. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
149
|
+
log(' 8. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
150
|
+
} else if (migrationStatus.defaultData !== 'success') {
|
|
151
|
+
log(' 2. Setup default data: ' + colors.bright + 'npm run seed-default' + colors.reset);
|
|
152
|
+
log(' 3. Generate API key: ' + colors.bright + 'npm run generate-key' + colors.reset + ' (after default data is created)');
|
|
153
|
+
log(' 4. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
154
|
+
log(' 5. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
155
|
+
log(' 6. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
156
|
+
log(' 7. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
157
|
+
} else if (migrationStatus.apiKey !== 'success') {
|
|
158
|
+
log(' 2. Generate API key: ' + colors.bright + 'npm run generate-key' + colors.reset + ' (for library integration)');
|
|
159
|
+
log(' 3. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
160
|
+
log(' 4. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
161
|
+
log(' 5. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
162
|
+
log(' 6. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
163
|
+
} else {
|
|
164
|
+
log(' 2. Start the server: ' + colors.bright + 'vezlo-server' + colors.reset);
|
|
165
|
+
log(' 3. Visit: ' + colors.bright + 'http://localhost:3000/health' + colors.reset);
|
|
166
|
+
log(' 4. API docs: ' + colors.bright + 'http://localhost:3000/docs' + colors.reset);
|
|
167
|
+
log(' 5. Test API: ' + colors.bright + 'curl http://localhost:3000/health' + colors.reset + '\n');
|
|
168
|
+
}
|
|
118
169
|
|
|
119
170
|
rl.close();
|
|
171
|
+
// Ensure graceful exit even if any handles remain
|
|
172
|
+
setImmediate(() => process.exit(0));
|
|
120
173
|
}
|
|
121
174
|
|
|
122
175
|
async function setupSupabase() {
|
|
123
176
|
log('\nš¦ Supabase Configuration\n', 'blue');
|
|
124
177
|
log('You can find these values in your Supabase Dashboard:', 'yellow');
|
|
125
|
-
log(' Settings > API > Project URL & API Keys
|
|
178
|
+
log(' ⢠API keys & URL: Settings > API > Project URL & API Keys', 'yellow');
|
|
179
|
+
log(' ⢠Database params: Settings > Database > Connection info', 'yellow');
|
|
180
|
+
log(' ⢠Optional pooling: Connect > Connection Pooling > Session Pooler > View parameters\n', 'yellow');
|
|
126
181
|
|
|
182
|
+
// Get Supabase URL and extract project ID for defaults
|
|
127
183
|
const supabaseUrl = await question('Supabase Project URL (https://xxx.supabase.co):');
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
184
|
+
const projectId = supabaseUrl.match(/https:\/\/(.+?)\.supabase\.co/)?.[1];
|
|
185
|
+
|
|
186
|
+
if (!projectId) {
|
|
187
|
+
log('\nā Invalid Supabase URL format. Please use: https://your-project.supabase.co', 'red');
|
|
188
|
+
throw new Error('Invalid Supabase URL');
|
|
189
|
+
}
|
|
133
190
|
|
|
191
|
+
const supabaseServiceKey = await question('Supabase Service Role Key:');
|
|
192
|
+
const supabaseAnonKey = await question('Supabase Anon Key (optional, press Enter to skip):');
|
|
193
|
+
|
|
194
|
+
// Show defaults and ask for each database parameter
|
|
195
|
+
log('\nš Database Connection Details:', 'blue');
|
|
196
|
+
|
|
197
|
+
const dbHost = await question(`Database Host (default: db.${projectId}.supabase.co):`) || `db.${projectId}.supabase.co`;
|
|
198
|
+
const dbPort = await question('Database Port (default: 5432):') || '5432';
|
|
199
|
+
const dbName = await question('Database Name (default: postgres):') || 'postgres';
|
|
200
|
+
const dbUser = await question(`Database User (default: postgres.${projectId}):`) || `postgres.${projectId}`;
|
|
201
|
+
const dbPassword = await question('Database Password (from Settings > Database):');
|
|
202
|
+
|
|
203
|
+
// Validate Supabase connection (same as validate script)
|
|
204
|
+
log('\nš Validating Supabase connection...', 'yellow');
|
|
205
|
+
|
|
134
206
|
try {
|
|
135
207
|
const client = createClient(supabaseUrl.trim(), supabaseServiceKey.trim());
|
|
136
|
-
const {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
208
|
+
const { error } = await client.from('vezlo_conversations').select('count').limit(0);
|
|
209
|
+
|
|
210
|
+
if (error) {
|
|
211
|
+
// Check for table not found errors (normal before migrations run)
|
|
212
|
+
if (error.code === 'PGRST116' ||
|
|
213
|
+
error.message.includes('does not exist') ||
|
|
214
|
+
error.message.includes('Could not find the table')) {
|
|
215
|
+
log('ā
Supabase connection successful!', 'green');
|
|
216
|
+
log('ā ļø Note: Table not found - this is normal before running migrations\n', 'yellow');
|
|
217
|
+
} else {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
142
220
|
} else {
|
|
143
|
-
log('ā
|
|
221
|
+
log('ā
Supabase connection successful!\n', 'green');
|
|
144
222
|
}
|
|
145
223
|
} catch (err) {
|
|
146
|
-
log(
|
|
147
|
-
log('
|
|
224
|
+
log(`ā Supabase connection failed: ${err.message}`, 'red');
|
|
225
|
+
log('ā ļø This might be because migrations haven\'t run yet, or check your credentials.', 'yellow');
|
|
148
226
|
}
|
|
149
227
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
228
|
+
// Validate database connection (same as validate script)
|
|
229
|
+
log('\nš Validating database connection...', 'yellow');
|
|
230
|
+
|
|
231
|
+
let client;
|
|
232
|
+
try {
|
|
233
|
+
const { Client } = require('pg');
|
|
234
|
+
|
|
235
|
+
client = new Client({
|
|
236
|
+
host: dbHost.trim(),
|
|
237
|
+
port: parseInt(dbPort.trim()),
|
|
238
|
+
database: dbName.trim(),
|
|
239
|
+
user: dbUser.trim(),
|
|
240
|
+
password: dbPassword.trim(),
|
|
241
|
+
ssl: { rejectUnauthorized: false }
|
|
242
|
+
});
|
|
153
243
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
244
|
+
// Handle connection errors quietly for normal shutdowns
|
|
245
|
+
client.on('error', (err) => {
|
|
246
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
247
|
+
if (msg && (msg.includes('client_termination') || msg.includes(':shutdown'))) {
|
|
248
|
+
return; // ignore normal termination noise
|
|
249
|
+
}
|
|
250
|
+
console.error('Database connection error:', msg);
|
|
251
|
+
});
|
|
159
252
|
|
|
160
|
-
|
|
253
|
+
await client.connect();
|
|
254
|
+
log('ā
Database connection successful!\n', 'green');
|
|
255
|
+
await client.end();
|
|
256
|
+
} catch (err) {
|
|
257
|
+
log(`ā Database connection failed: ${err.message}`, 'red');
|
|
258
|
+
log('ā ļø Continuing setup. Migrations will be skipped; see summary for next steps.', 'yellow');
|
|
259
|
+
}
|
|
161
260
|
|
|
162
261
|
return {
|
|
163
262
|
SUPABASE_URL: supabaseUrl.trim(),
|
|
164
|
-
SUPABASE_ANON_KEY: supabaseAnonKey.trim(),
|
|
263
|
+
SUPABASE_ANON_KEY: supabaseAnonKey.trim() || '',
|
|
165
264
|
SUPABASE_SERVICE_KEY: supabaseServiceKey.trim(),
|
|
166
|
-
SUPABASE_DB_HOST: dbHost,
|
|
167
|
-
SUPABASE_DB_PORT:
|
|
168
|
-
SUPABASE_DB_NAME:
|
|
169
|
-
SUPABASE_DB_USER:
|
|
265
|
+
SUPABASE_DB_HOST: dbHost.trim(),
|
|
266
|
+
SUPABASE_DB_PORT: dbPort.trim(),
|
|
267
|
+
SUPABASE_DB_NAME: dbName.trim(),
|
|
268
|
+
SUPABASE_DB_USER: dbUser.trim(),
|
|
170
269
|
SUPABASE_DB_PASSWORD: dbPassword.trim(),
|
|
171
270
|
PORT: '3000',
|
|
172
271
|
NODE_ENV: 'development',
|
|
173
|
-
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173'
|
|
272
|
+
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173',
|
|
273
|
+
BASE_URL: 'http://localhost:3000'
|
|
174
274
|
};
|
|
175
275
|
}
|
|
176
276
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const user = await question('Database User (postgres):') || 'postgres';
|
|
184
|
-
const password = await question('Database Password:');
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
SUPABASE_DB_HOST: host.trim(),
|
|
188
|
-
SUPABASE_DB_PORT: port.trim(),
|
|
189
|
-
SUPABASE_DB_NAME: database.trim(),
|
|
190
|
-
SUPABASE_DB_USER: user.trim(),
|
|
191
|
-
SUPABASE_DB_PASSWORD: password.trim(),
|
|
192
|
-
PORT: '3000',
|
|
193
|
-
NODE_ENV: 'development',
|
|
194
|
-
CORS_ORIGINS: 'http://localhost:3000,http://localhost:5173'
|
|
195
|
-
};
|
|
196
|
-
}
|
|
277
|
+
// Handle errors and cleanup
|
|
278
|
+
process.on('SIGINT', () => {
|
|
279
|
+
log('\n\nā ļø Setup cancelled by user', 'yellow');
|
|
280
|
+
rl.close();
|
|
281
|
+
process.exit(0);
|
|
282
|
+
});
|
|
197
283
|
|
|
198
|
-
|
|
199
|
-
|
|
284
|
+
// Run the wizard
|
|
285
|
+
main().catch(error => {
|
|
286
|
+
log(`\nā Setup failed: ${error.message}`, 'red');
|
|
287
|
+
rl.close();
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
200
290
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
async function saveEnvFile(envPath, config) {
|
|
292
|
+
// Don't overwrite existing .env
|
|
293
|
+
if (fs.existsSync(envPath)) {
|
|
294
|
+
log('\nā ļø .env already exists. Skipping overwrite. Please review values manually.', 'yellow');
|
|
295
|
+
return false;
|
|
204
296
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
envContent.split('\n').forEach(line => {
|
|
212
|
-
const match = line.match(/^([^=:#]+)=(.*)$/);
|
|
213
|
-
if (match) {
|
|
214
|
-
const key = match[1].trim();
|
|
215
|
-
const value = match[2].trim();
|
|
216
|
-
config[key] = value;
|
|
297
|
+
// Generate a secure migration secret if not provided
|
|
298
|
+
try {
|
|
299
|
+
const crypto = require('crypto');
|
|
300
|
+
if (!config.MIGRATION_SECRET_KEY) {
|
|
301
|
+
config.MIGRATION_SECRET_KEY = crypto.randomBytes(32).toString('hex');
|
|
217
302
|
}
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function saveEnvFile(envPath, config) {
|
|
303
|
+
} catch (_) {
|
|
304
|
+
// Fallback simple key if crypto unavailable (very unlikely)
|
|
305
|
+
config.MIGRATION_SECRET_KEY = config.MIGRATION_SECRET_KEY || `msk_${Date.now()}`;
|
|
306
|
+
}
|
|
224
307
|
const envContent = `# Vezlo Assistant Server Configuration
|
|
225
308
|
# Generated by setup wizard on ${new Date().toISOString()}
|
|
226
309
|
|
|
@@ -232,6 +315,9 @@ LOG_LEVEL=info
|
|
|
232
315
|
# CORS Configuration
|
|
233
316
|
CORS_ORIGINS=${config.CORS_ORIGINS || 'http://localhost:3000,http://localhost:5173'}
|
|
234
317
|
|
|
318
|
+
# Swagger Base URL
|
|
319
|
+
BASE_URL=${config.BASE_URL || 'http://localhost:3000'}
|
|
320
|
+
|
|
235
321
|
# Rate Limiting
|
|
236
322
|
RATE_LIMIT_WINDOW=60000
|
|
237
323
|
RATE_LIMIT_MAX=100
|
|
@@ -251,8 +337,8 @@ SUPABASE_DB_PASSWORD=${config.SUPABASE_DB_PASSWORD || ''}
|
|
|
251
337
|
# OpenAI Configuration
|
|
252
338
|
OPENAI_API_KEY=${config.OPENAI_API_KEY || 'sk-your-openai-api-key'}
|
|
253
339
|
AI_MODEL=${config.AI_MODEL || 'gpt-4o'}
|
|
254
|
-
AI_TEMPERATURE
|
|
255
|
-
AI_MAX_TOKENS
|
|
340
|
+
AI_TEMPERATURE=${config.AI_TEMPERATURE || '0.7'}
|
|
341
|
+
AI_MAX_TOKENS=${config.AI_MAX_TOKENS || '1000'}
|
|
256
342
|
|
|
257
343
|
# Organization Settings
|
|
258
344
|
ORGANIZATION_NAME=Vezlo
|
|
@@ -261,66 +347,174 @@ ASSISTANT_NAME=Vezlo Assistant
|
|
|
261
347
|
# Knowledge Base
|
|
262
348
|
CHUNK_SIZE=1000
|
|
263
349
|
CHUNK_OVERLAP=200
|
|
350
|
+
|
|
351
|
+
# Migration Security
|
|
352
|
+
MIGRATION_SECRET_KEY=${config.MIGRATION_SECRET_KEY}
|
|
264
353
|
`;
|
|
265
354
|
|
|
266
355
|
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
356
|
+
return true;
|
|
267
357
|
}
|
|
268
358
|
|
|
269
|
-
async function
|
|
270
|
-
log('
|
|
359
|
+
async function validateEnvironment(config) {
|
|
360
|
+
log('š Validating environment configuration...', 'yellow');
|
|
271
361
|
|
|
362
|
+
// Test Supabase connection (same as validate script)
|
|
272
363
|
try {
|
|
273
|
-
const
|
|
364
|
+
const client = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_KEY);
|
|
365
|
+
const { error } = await client.from('vezlo_conversations').select('count').limit(0);
|
|
366
|
+
|
|
367
|
+
if (error) {
|
|
368
|
+
// Check for table not found errors (normal before migrations run)
|
|
369
|
+
if (error.code === 'PGRST116' ||
|
|
370
|
+
error.message.includes('does not exist') ||
|
|
371
|
+
error.message.includes('Could not find the table')) {
|
|
372
|
+
log('ā
Supabase API connection validated', 'green');
|
|
373
|
+
log('ā ļø Note: Table not found - this is normal before running migrations', 'yellow');
|
|
374
|
+
} else {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
log('ā
Supabase API connection validated', 'green');
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log(`ā Supabase API validation failed: ${err.message}`, 'red');
|
|
382
|
+
// non-blocking; proceed to DB check anyway
|
|
383
|
+
// return partial status so caller can decide
|
|
384
|
+
// (we'll still attempt DB validation)
|
|
385
|
+
}
|
|
274
386
|
|
|
275
|
-
|
|
387
|
+
// Test database connection (same as validate script)
|
|
388
|
+
log('\nš Validating database connection...', 'yellow');
|
|
389
|
+
let client;
|
|
390
|
+
try {
|
|
391
|
+
const { Client } = require('pg');
|
|
392
|
+
|
|
393
|
+
client = new Client({
|
|
276
394
|
host: config.SUPABASE_DB_HOST,
|
|
277
|
-
port: parseInt(config.SUPABASE_DB_PORT
|
|
395
|
+
port: parseInt(config.SUPABASE_DB_PORT),
|
|
278
396
|
database: config.SUPABASE_DB_NAME,
|
|
279
397
|
user: config.SUPABASE_DB_USER,
|
|
280
398
|
password: config.SUPABASE_DB_PASSWORD,
|
|
281
399
|
ssl: { rejectUnauthorized: false }
|
|
282
400
|
});
|
|
283
401
|
|
|
402
|
+
// Handle connection errors quietly for normal shutdowns
|
|
403
|
+
client.on('error', (err) => {
|
|
404
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
405
|
+
if (msg && (msg.includes('client_termination') || msg.includes(':shutdown'))) {
|
|
406
|
+
return; // ignore normal termination noise
|
|
407
|
+
}
|
|
408
|
+
console.error('Database connection error:', msg);
|
|
409
|
+
});
|
|
410
|
+
|
|
284
411
|
await client.connect();
|
|
285
|
-
log('ā
|
|
412
|
+
log('ā
Database connection validated', 'green');
|
|
413
|
+
await client.end();
|
|
414
|
+
} catch (err) {
|
|
415
|
+
log(`ā Database validation failed: ${err.message}`, 'red');
|
|
416
|
+
log('ā ļø Continuing setup; migrations will be skipped.', 'yellow');
|
|
417
|
+
return { supabaseApi: 'unknown', database: 'failed' };
|
|
418
|
+
}
|
|
286
419
|
|
|
287
|
-
|
|
288
|
-
|
|
420
|
+
log('ā
Environment validation complete!\n', 'green');
|
|
421
|
+
return { supabaseApi: 'success', database: 'success' };
|
|
422
|
+
}
|
|
289
423
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
424
|
+
async function setupMigrations(config, validationStatus) {
|
|
425
|
+
log('š Checking migration status...', 'yellow');
|
|
294
426
|
|
|
295
|
-
|
|
427
|
+
if (validationStatus.database !== 'success') {
|
|
428
|
+
log('ā ļø Skipping migrations because database validation failed.', 'yellow');
|
|
429
|
+
return { migrations: 'skipped' };
|
|
430
|
+
}
|
|
296
431
|
|
|
297
|
-
|
|
298
|
-
|
|
432
|
+
try {
|
|
433
|
+
const { Client } = require('pg');
|
|
434
|
+
const client = new Client({
|
|
435
|
+
host: config.SUPABASE_DB_HOST,
|
|
436
|
+
port: parseInt(config.SUPABASE_DB_PORT),
|
|
437
|
+
database: config.SUPABASE_DB_NAME,
|
|
438
|
+
user: config.SUPABASE_DB_USER,
|
|
439
|
+
password: config.SUPABASE_DB_PASSWORD,
|
|
440
|
+
ssl: { rejectUnauthorized: false }
|
|
441
|
+
});
|
|
299
442
|
|
|
300
|
-
|
|
443
|
+
await client.connect();
|
|
301
444
|
|
|
302
|
-
//
|
|
303
|
-
const
|
|
304
|
-
SELECT
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
445
|
+
// Check if migrations table exists
|
|
446
|
+
const migrationTableExists = await client.query(`
|
|
447
|
+
SELECT EXISTS (
|
|
448
|
+
SELECT FROM information_schema.tables
|
|
449
|
+
WHERE table_schema = 'public'
|
|
450
|
+
AND table_name = 'knex_migrations'
|
|
451
|
+
);
|
|
309
452
|
`);
|
|
310
453
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
454
|
+
if (!migrationTableExists.rows[0].exists) {
|
|
455
|
+
log('š No migrations table found. Database needs initial setup.', 'blue');
|
|
456
|
+
|
|
457
|
+
const runMigrations = await question('Run initial database migrations now? (y/n):');
|
|
458
|
+
|
|
459
|
+
if (runMigrations.toLowerCase() === 'y') {
|
|
460
|
+
log('š Running migrations...', 'yellow');
|
|
461
|
+
|
|
462
|
+
// Set environment variables for migration
|
|
463
|
+
process.env.SUPABASE_DB_HOST = config.SUPABASE_DB_HOST;
|
|
464
|
+
process.env.SUPABASE_DB_PORT = config.SUPABASE_DB_PORT;
|
|
465
|
+
process.env.SUPABASE_DB_NAME = config.SUPABASE_DB_NAME;
|
|
466
|
+
process.env.SUPABASE_DB_USER = config.SUPABASE_DB_USER;
|
|
467
|
+
process.env.SUPABASE_DB_PASSWORD = config.SUPABASE_DB_PASSWORD;
|
|
468
|
+
|
|
469
|
+
const { execSync } = require('child_process');
|
|
470
|
+
execSync('npm run migrate:latest', { stdio: 'inherit' });
|
|
471
|
+
|
|
472
|
+
log('ā
Migrations completed successfully!', 'green');
|
|
473
|
+
return { migrations: 'success' };
|
|
474
|
+
} else {
|
|
475
|
+
log('\nā ļø Migrations skipped by user.', 'yellow');
|
|
476
|
+
log(' You can run them later using: ' + colors.cyan + 'npm run migrate:latest' + colors.reset, 'yellow');
|
|
477
|
+
log(' Or via API: ' + colors.cyan + 'GET /api/migrate?key=your-migration-secret' + colors.reset, 'yellow');
|
|
478
|
+
log('\n ā ļø Note: Default data seeding will be skipped until migrations are run.\n', 'yellow');
|
|
479
|
+
return { migrations: 'skipped' };
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
// Check migration status
|
|
483
|
+
const migrationStatus = await client.query(`
|
|
484
|
+
SELECT COUNT(*) as count FROM knex_migrations;
|
|
485
|
+
`);
|
|
486
|
+
|
|
487
|
+
log(`š Found ${migrationStatus.rows[0].count} completed migrations`, 'blue');
|
|
488
|
+
|
|
489
|
+
const runPending = await question('Check for pending migrations? (y/n):');
|
|
490
|
+
|
|
491
|
+
if (runPending.toLowerCase() === 'y') {
|
|
492
|
+
log('š Checking for pending migrations...', 'yellow');
|
|
493
|
+
|
|
494
|
+
const { execSync } = require('child_process');
|
|
495
|
+
execSync('npm run migrate:latest', { stdio: 'inherit' });
|
|
496
|
+
|
|
497
|
+
log('ā
Migration check completed!', 'green');
|
|
498
|
+
await client.end();
|
|
499
|
+
return { migrations: 'success' };
|
|
500
|
+
} else {
|
|
501
|
+
log('ā ļø Migration check skipped by user.', 'yellow');
|
|
502
|
+
log(' You can run them later using: ' + colors.cyan + 'npm run migrate:latest' + colors.reset, 'yellow');
|
|
503
|
+
log(' Or via API: ' + colors.cyan + 'GET /api/migrate?key=your-migration-secret' + colors.reset, 'yellow');
|
|
504
|
+
log('\n ā ļø Note: Default data seeding will be skipped until migrations are run.\n', 'yellow');
|
|
505
|
+
await client.end();
|
|
506
|
+
return { migrations: 'skipped' };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
316
509
|
|
|
317
510
|
await client.end();
|
|
318
|
-
|
|
319
|
-
} catch (
|
|
320
|
-
log(
|
|
321
|
-
log('\nYou can
|
|
322
|
-
log('
|
|
323
|
-
log('
|
|
511
|
+
return { migrations: 'skipped' };
|
|
512
|
+
} catch (err) {
|
|
513
|
+
log(`ā Migration setup failed: ${err.message}`, 'red');
|
|
514
|
+
log('\nYou can run migrations manually later:', 'yellow');
|
|
515
|
+
log(' npm run migrate:latest', 'cyan');
|
|
516
|
+
log(' Or via API: GET /api/migrate?key=your-migration-secret\n', 'cyan');
|
|
517
|
+
return { migrations: 'failed' };
|
|
324
518
|
}
|
|
325
519
|
}
|
|
326
520
|
|
|
@@ -337,3 +531,70 @@ main().catch(error => {
|
|
|
337
531
|
rl.close();
|
|
338
532
|
process.exit(1);
|
|
339
533
|
});
|
|
534
|
+
async function setupDefaultData(config) {
|
|
535
|
+
log('š Setting up default company and admin user...', 'yellow');
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
// Import the setup service dynamically
|
|
539
|
+
const { runDefaultSetup } = await import('./seed-default.js');
|
|
540
|
+
|
|
541
|
+
// Set environment variables for the setup
|
|
542
|
+
process.env.SUPABASE_URL = config.SUPABASE_URL;
|
|
543
|
+
process.env.SUPABASE_SERVICE_KEY = config.SUPABASE_SERVICE_KEY;
|
|
544
|
+
process.env.DEFAULT_ADMIN_EMAIL = config.DEFAULT_ADMIN_EMAIL || 'admin@vezlo.org';
|
|
545
|
+
process.env.DEFAULT_ADMIN_PASSWORD = config.DEFAULT_ADMIN_PASSWORD || 'admin123';
|
|
546
|
+
process.env.ORGANIZATION_NAME = config.ORGANIZATION_NAME || 'Vezlo';
|
|
547
|
+
process.env.JWT_SECRET = config.JWT_SECRET || require('crypto').randomBytes(32).toString('hex');
|
|
548
|
+
|
|
549
|
+
// Run the default setup
|
|
550
|
+
await runDefaultSetup();
|
|
551
|
+
|
|
552
|
+
log('ā
Default data setup completed successfully!', 'green');
|
|
553
|
+
return 'success';
|
|
554
|
+
} catch (err) {
|
|
555
|
+
log(`ā Default data setup failed: ${err.message}`, 'red');
|
|
556
|
+
log('\nYou can run default data setup manually later:', 'yellow');
|
|
557
|
+
log(' npm run seed-default', 'cyan');
|
|
558
|
+
return 'failed';
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function setupApiKey(config) {
|
|
563
|
+
log('š Generating API key for library integration...', 'yellow');
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
// Import the API key generator dynamically
|
|
567
|
+
const { generateApiKey } = await import('./generate-key.js');
|
|
568
|
+
|
|
569
|
+
// Set environment variables for the setup (already set in setupDefaultData)
|
|
570
|
+
|
|
571
|
+
// Run the API key generator in quiet mode
|
|
572
|
+
const result = await generateApiKey({ quiet: true });
|
|
573
|
+
|
|
574
|
+
if (result.success) {
|
|
575
|
+
// Show API key details
|
|
576
|
+
log('ā
API key generated successfully!', 'green');
|
|
577
|
+
log('\nš API Key Details:', 'bright');
|
|
578
|
+
log(` Company: ${result.company}`, 'reset');
|
|
579
|
+
log(` User: ${result.user.name}`, 'reset');
|
|
580
|
+
log(` API Key: ${result.apiKey}`, 'bright');
|
|
581
|
+
log('\nā ļø IMPORTANT: Save this key securely. It will not be shown again.', 'yellow');
|
|
582
|
+
|
|
583
|
+
// Show usage example
|
|
584
|
+
log('\nš§ Usage Example:', 'bright');
|
|
585
|
+
log(` curl -X POST http://localhost:3000/api/knowledge/items \\
|
|
586
|
+
-H "X-API-Key: ${result.apiKey}" \\
|
|
587
|
+
-H "Content-Type: application/json" \\
|
|
588
|
+
-d '{"title": "Example", "type": "document", "content": "Example content"}'`, 'cyan');
|
|
589
|
+
|
|
590
|
+
return 'success';
|
|
591
|
+
} else {
|
|
592
|
+
throw new Error(result.error);
|
|
593
|
+
}
|
|
594
|
+
} catch (err) {
|
|
595
|
+
log(`ā API key generation failed: ${err.message}`, 'red');
|
|
596
|
+
log('\nYou can generate an API key manually later:', 'yellow');
|
|
597
|
+
log(' npm run generate-key', 'cyan');
|
|
598
|
+
return 'failed';
|
|
599
|
+
}
|
|
600
|
+
}
|