@friggframework/devtools 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.dba001a.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/frigg-cli/README.md +1 -1
- package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
- package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
- package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
- package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
- package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
- package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
- package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
- package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
- package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
- package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
- package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
- package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
- package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
- package/frigg-cli/build-command/index.js +123 -11
- package/frigg-cli/container.js +172 -0
- package/frigg-cli/deploy-command/index.js +83 -1
- package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
- package/frigg-cli/doctor-command/index.js +37 -16
- package/frigg-cli/domain/entities/ApiModule.js +272 -0
- package/frigg-cli/domain/entities/AppDefinition.js +227 -0
- package/frigg-cli/domain/entities/Integration.js +198 -0
- package/frigg-cli/domain/exceptions/DomainException.js +24 -0
- package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
- package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
- package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
- package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
- package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
- package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
- package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
- package/frigg-cli/generate-iam-command.js +21 -1
- package/frigg-cli/index.js +21 -6
- package/frigg-cli/index.test.js +7 -2
- package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
- package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
- package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
- package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
- package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
- package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
- package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
- package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
- package/frigg-cli/init-command/backend-first-handler.js +124 -42
- package/frigg-cli/init-command/index.js +2 -1
- package/frigg-cli/init-command/template-handler.js +13 -3
- package/frigg-cli/install-command/backend-js.js +3 -3
- package/frigg-cli/install-command/environment-variables.js +16 -19
- package/frigg-cli/install-command/environment-variables.test.js +12 -13
- package/frigg-cli/install-command/index.js +14 -9
- package/frigg-cli/install-command/integration-file.js +3 -3
- package/frigg-cli/install-command/validate-package.js +5 -9
- package/frigg-cli/jest.config.js +4 -1
- package/frigg-cli/package-lock.json +16226 -0
- package/frigg-cli/repair-command/index.js +121 -128
- package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
- package/frigg-cli/start-command/index.js +324 -2
- package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
- package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
- package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
- package/frigg-cli/templates/backend/.env.example +62 -0
- package/frigg-cli/templates/backend/.eslintrc.json +12 -0
- package/frigg-cli/templates/backend/.prettierrc +6 -0
- package/frigg-cli/templates/backend/docker-compose.yml +22 -0
- package/frigg-cli/templates/backend/index.js +96 -0
- package/frigg-cli/templates/backend/infrastructure.js +12 -0
- package/frigg-cli/templates/backend/jest.config.js +17 -0
- package/frigg-cli/templates/backend/package.json +50 -0
- package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
- package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
- package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
- package/frigg-cli/templates/backend/test/setup.js +30 -0
- package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
- package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
- package/frigg-cli/ui-command/index.js +58 -36
- package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
- package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
- package/frigg-cli/utils/output.js +382 -0
- package/frigg-cli/utils/provider-helper.js +75 -0
- package/frigg-cli/utils/repo-detection.js +85 -37
- package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
- package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
- package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
- package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
- package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
- package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
- package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
- package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
- package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
- package/infrastructure/create-frigg-infrastructure.js +93 -0
- package/infrastructure/docs/iam-policy-templates.md +1 -1
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
- package/infrastructure/domains/admin-scripts/index.js +5 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
- package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
- package/infrastructure/domains/shared/resource-discovery.js +5 -5
- package/infrastructure/domains/shared/types/app-definition.js +21 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
- package/infrastructure/infrastructure-composer.js +2 -0
- package/infrastructure/infrastructure-composer.test.js +2 -2
- package/infrastructure/jest.config.js +16 -0
- package/management-ui/README.md +245 -109
- package/package.json +8 -7
- package/frigg-cli/install-command/logger.js +0 -12
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatabaseAdapter - Infrastructure adapter for database connectivity checks
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to parse connection strings and test database reachability
|
|
5
|
+
* Used by pre-flight checks to verify database infrastructure is ready
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
class DatabaseAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Parse a database connection string
|
|
15
|
+
* @param {string} connectionString - Database URL
|
|
16
|
+
* @returns {object} Parsed connection details or error
|
|
17
|
+
*/
|
|
18
|
+
parseConnectionString(connectionString) {
|
|
19
|
+
if (!connectionString || typeof connectionString !== 'string') {
|
|
20
|
+
return { error: 'Invalid connection string: must be a non-empty string' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Handle mongodb+srv:// protocol separately (URL doesn't parse it well)
|
|
25
|
+
if (connectionString.startsWith('mongodb+srv://')) {
|
|
26
|
+
return this._parseMongoSrv(connectionString);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle MongoDB replica set connection strings with multiple hosts
|
|
30
|
+
// mongodb://host1:port1,host2:port2,host3:port3/database
|
|
31
|
+
if (connectionString.startsWith('mongodb://') && connectionString.includes(',')) {
|
|
32
|
+
return this._parseMongoReplicaSet(connectionString);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const url = new URL(connectionString);
|
|
36
|
+
const protocol = url.protocol.replace(':', '');
|
|
37
|
+
|
|
38
|
+
// Determine database type
|
|
39
|
+
let type;
|
|
40
|
+
let defaultPort;
|
|
41
|
+
if (protocol === 'mongodb') {
|
|
42
|
+
type = 'mongodb';
|
|
43
|
+
defaultPort = 27017;
|
|
44
|
+
} else if (protocol === 'postgresql' || protocol === 'postgres') {
|
|
45
|
+
type = 'postgresql';
|
|
46
|
+
defaultPort = 5432;
|
|
47
|
+
} else {
|
|
48
|
+
return { error: `Unsupported database type: ${protocol}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Extract first host from potential replica set
|
|
52
|
+
const host = url.hostname.split(',')[0];
|
|
53
|
+
const port = url.port ? parseInt(url.port, 10) : defaultPort;
|
|
54
|
+
|
|
55
|
+
// Extract database name from pathname
|
|
56
|
+
const database = url.pathname.replace('/', '') || undefined;
|
|
57
|
+
|
|
58
|
+
// Extract user if present
|
|
59
|
+
const user = url.username || undefined;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type,
|
|
63
|
+
host,
|
|
64
|
+
port,
|
|
65
|
+
database,
|
|
66
|
+
user
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { error: `Failed to parse connection string: ${error.message}` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse MongoDB replica set connection string with multiple hosts
|
|
75
|
+
* @param {string} connectionString - MongoDB replica set connection string
|
|
76
|
+
* @returns {object} Parsed connection details
|
|
77
|
+
*/
|
|
78
|
+
_parseMongoReplicaSet(connectionString) {
|
|
79
|
+
try {
|
|
80
|
+
// mongodb://user:pass@host1:port1,host2:port2/database?options
|
|
81
|
+
const withoutProtocol = connectionString.replace('mongodb://', '');
|
|
82
|
+
|
|
83
|
+
// Extract user:pass if present
|
|
84
|
+
let hostsPart = withoutProtocol;
|
|
85
|
+
let user;
|
|
86
|
+
if (withoutProtocol.includes('@')) {
|
|
87
|
+
const atIndex = withoutProtocol.indexOf('@');
|
|
88
|
+
const credentials = withoutProtocol.substring(0, atIndex);
|
|
89
|
+
hostsPart = withoutProtocol.substring(atIndex + 1);
|
|
90
|
+
user = credentials.split(':')[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract database and options
|
|
94
|
+
let database;
|
|
95
|
+
const slashIndex = hostsPart.indexOf('/');
|
|
96
|
+
if (slashIndex !== -1) {
|
|
97
|
+
const pathPart = hostsPart.substring(slashIndex + 1);
|
|
98
|
+
hostsPart = hostsPart.substring(0, slashIndex);
|
|
99
|
+
database = pathPart.split('?')[0] || undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get first host:port pair
|
|
103
|
+
const firstHost = hostsPart.split(',')[0];
|
|
104
|
+
const [host, portStr] = firstHost.split(':');
|
|
105
|
+
const port = portStr ? parseInt(portStr, 10) : 27017;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
type: 'mongodb',
|
|
109
|
+
host,
|
|
110
|
+
port,
|
|
111
|
+
database,
|
|
112
|
+
user
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return { error: `Failed to parse MongoDB replica set connection string: ${error.message}` };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse mongodb+srv:// connection string
|
|
121
|
+
* @param {string} connectionString - MongoDB SRV connection string
|
|
122
|
+
* @returns {object} Parsed connection details
|
|
123
|
+
*/
|
|
124
|
+
_parseMongoSrv(connectionString) {
|
|
125
|
+
try {
|
|
126
|
+
// Replace mongodb+srv with https for URL parsing
|
|
127
|
+
const url = new URL(connectionString.replace('mongodb+srv://', 'https://'));
|
|
128
|
+
|
|
129
|
+
const host = url.hostname;
|
|
130
|
+
const database = url.pathname.replace('/', '') || undefined;
|
|
131
|
+
const user = url.username || undefined;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
type: 'mongodb',
|
|
135
|
+
host,
|
|
136
|
+
port: 27017, // SRV uses standard port
|
|
137
|
+
database,
|
|
138
|
+
user
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { error: `Failed to parse MongoDB SRV connection string: ${error.message}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get database type from connection string
|
|
147
|
+
* @param {string} connectionString - Database URL
|
|
148
|
+
* @returns {string|null} 'mongodb' | 'postgresql' | null
|
|
149
|
+
*/
|
|
150
|
+
getDatabaseType(connectionString) {
|
|
151
|
+
if (!connectionString) return null;
|
|
152
|
+
|
|
153
|
+
if (connectionString.startsWith('mongodb://') || connectionString.startsWith('mongodb+srv://')) {
|
|
154
|
+
return 'mongodb';
|
|
155
|
+
}
|
|
156
|
+
if (connectionString.startsWith('postgresql://') || connectionString.startsWith('postgres://')) {
|
|
157
|
+
return 'postgresql';
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if a TCP port is reachable
|
|
164
|
+
* @param {string} host - Host to connect to
|
|
165
|
+
* @param {number} port - Port to connect to
|
|
166
|
+
* @param {number} timeout - Timeout in milliseconds (default: 3000)
|
|
167
|
+
* @returns {Promise<boolean>} True if port is reachable
|
|
168
|
+
*/
|
|
169
|
+
async isPortReachable(host, port, timeout = 3000) {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
const socket = net.createConnection(port, host);
|
|
172
|
+
|
|
173
|
+
socket.setTimeout(timeout);
|
|
174
|
+
|
|
175
|
+
socket.on('connect', () => {
|
|
176
|
+
socket.destroy();
|
|
177
|
+
resolve(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
socket.on('timeout', () => {
|
|
181
|
+
socket.destroy();
|
|
182
|
+
resolve(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
socket.on('error', () => {
|
|
186
|
+
socket.destroy();
|
|
187
|
+
resolve(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if database is reachable at the connection string
|
|
194
|
+
* @param {string} connectionString - Database URL
|
|
195
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
196
|
+
* @returns {Promise<{reachable: boolean, host?: string, port?: number, type?: string, error?: string}>}
|
|
197
|
+
*/
|
|
198
|
+
async isDatabaseReachable(connectionString, timeout = 3000) {
|
|
199
|
+
const parsed = this.parseConnectionString(connectionString);
|
|
200
|
+
|
|
201
|
+
if (parsed.error) {
|
|
202
|
+
return {
|
|
203
|
+
reachable: false,
|
|
204
|
+
error: parsed.error
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { type, host, port } = parsed;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const reachable = await this.isPortReachable(host, port, timeout);
|
|
212
|
+
|
|
213
|
+
if (reachable) {
|
|
214
|
+
return {
|
|
215
|
+
reachable: true,
|
|
216
|
+
type,
|
|
217
|
+
host,
|
|
218
|
+
port
|
|
219
|
+
};
|
|
220
|
+
} else {
|
|
221
|
+
return {
|
|
222
|
+
reachable: false,
|
|
223
|
+
type,
|
|
224
|
+
host,
|
|
225
|
+
port,
|
|
226
|
+
error: `ECONNREFUSED - Unable to connect to ${host}:${port}`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
reachable: false,
|
|
232
|
+
type,
|
|
233
|
+
host,
|
|
234
|
+
port,
|
|
235
|
+
error: error.message
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get connection details summary
|
|
242
|
+
* @param {string} connectionString - Database URL
|
|
243
|
+
* @returns {object} Connection details or error
|
|
244
|
+
*/
|
|
245
|
+
getConnectionDetails(connectionString) {
|
|
246
|
+
const parsed = this.parseConnectionString(connectionString);
|
|
247
|
+
|
|
248
|
+
if (parsed.error) {
|
|
249
|
+
return { error: parsed.error };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
type: parsed.type,
|
|
254
|
+
host: parsed.host,
|
|
255
|
+
port: parsed.port,
|
|
256
|
+
database: parsed.database,
|
|
257
|
+
user: parsed.user,
|
|
258
|
+
hasCredentials: !!parsed.user
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Suggest Docker service configuration for a database type
|
|
264
|
+
* @param {string} dbType - 'mongodb' | 'postgresql'
|
|
265
|
+
* @returns {object|null} Docker service suggestion or null
|
|
266
|
+
*/
|
|
267
|
+
suggestDockerService(dbType) {
|
|
268
|
+
if (dbType === 'mongodb') {
|
|
269
|
+
return {
|
|
270
|
+
serviceName: 'mongodb',
|
|
271
|
+
image: 'mongo:7',
|
|
272
|
+
port: 27017,
|
|
273
|
+
envVars: {
|
|
274
|
+
MONGO_INITDB_DATABASE: 'frigg'
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (dbType === 'postgresql') {
|
|
280
|
+
return {
|
|
281
|
+
serviceName: 'postgres',
|
|
282
|
+
image: 'postgres:16',
|
|
283
|
+
port: 5432,
|
|
284
|
+
envVars: {
|
|
285
|
+
POSTGRES_DB: 'frigg',
|
|
286
|
+
POSTGRES_USER: 'postgres',
|
|
287
|
+
POSTGRES_PASSWORD: 'postgres'
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check PostgreSQL migration status using Prisma
|
|
297
|
+
* @param {string} projectPath - Path to the project
|
|
298
|
+
* @returns {Promise<{migrated: boolean, pendingMigrations?: string[], error?: string}>}
|
|
299
|
+
*/
|
|
300
|
+
async checkMigrationStatus(projectPath) {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
// Look for prisma schema in common locations
|
|
303
|
+
// Frigg apps use the schema from @friggframework/core
|
|
304
|
+
const possibleSchemaPaths = [
|
|
305
|
+
// Project-local prisma schemas
|
|
306
|
+
path.join(projectPath, 'prisma', 'postgresql', 'schema.prisma'),
|
|
307
|
+
path.join(projectPath, 'prisma', 'schema.prisma'),
|
|
308
|
+
// Frigg core schemas (in node_modules)
|
|
309
|
+
path.join(projectPath, 'node_modules', '@friggframework', 'core', 'prisma-postgresql', 'schema.prisma'),
|
|
310
|
+
path.join(projectPath, 'node_modules', '@friggframework', 'core', 'generated', 'prisma-postgresql', 'schema.prisma')
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
// Find the first existing schema path
|
|
314
|
+
const fs = require('fs');
|
|
315
|
+
let schemaPath = null;
|
|
316
|
+
for (const p of possibleSchemaPaths) {
|
|
317
|
+
if (fs.existsSync(p)) {
|
|
318
|
+
schemaPath = p;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!schemaPath) {
|
|
324
|
+
// No schema found - might be MongoDB project
|
|
325
|
+
resolve({ migrated: true, note: 'No Prisma PostgreSQL schema found' });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Find prisma binary - check local node_modules first
|
|
330
|
+
const possiblePrismaPaths = [
|
|
331
|
+
path.join(projectPath, 'node_modules', '.bin', 'prisma'),
|
|
332
|
+
path.join(projectPath, 'node_modules', 'prisma', 'build', 'index.js')
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
let prismaBin = 'npx';
|
|
336
|
+
let args = ['prisma', 'migrate', 'status', '--schema', schemaPath];
|
|
337
|
+
|
|
338
|
+
// Try to find local prisma binary for pnpm/yarn workspaces
|
|
339
|
+
for (const p of possiblePrismaPaths) {
|
|
340
|
+
if (fs.existsSync(p)) {
|
|
341
|
+
if (p.endsWith('.js')) {
|
|
342
|
+
prismaBin = 'node';
|
|
343
|
+
args = [p, 'migrate', 'status', '--schema', schemaPath];
|
|
344
|
+
} else {
|
|
345
|
+
prismaBin = p;
|
|
346
|
+
args = ['migrate', 'status', '--schema', schemaPath];
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const child = spawn(prismaBin, args, {
|
|
353
|
+
cwd: projectPath,
|
|
354
|
+
env: { ...process.env },
|
|
355
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
356
|
+
shell: prismaBin === 'npx' // Use shell for npx to help with path resolution
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
let stdout = '';
|
|
360
|
+
let stderr = '';
|
|
361
|
+
|
|
362
|
+
child.stdout.on('data', (data) => {
|
|
363
|
+
stdout += data.toString();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
child.stderr.on('data', (data) => {
|
|
367
|
+
stderr += data.toString();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
child.on('error', (error) => {
|
|
371
|
+
resolve({
|
|
372
|
+
migrated: false,
|
|
373
|
+
error: `Failed to run prisma migrate status: ${error.message}`
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
child.on('close', (code) => {
|
|
378
|
+
// Parse the output to determine migration status
|
|
379
|
+
const output = stdout + stderr;
|
|
380
|
+
|
|
381
|
+
// Check for Prisma not found / not installed error
|
|
382
|
+
if (output.includes('Cannot find module') && output.includes('prisma')) {
|
|
383
|
+
resolve({
|
|
384
|
+
migrated: false,
|
|
385
|
+
error: 'Prisma CLI not properly installed. Run "npm install" or "pnpm install" to fix.',
|
|
386
|
+
needsInstall: true
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check for common error patterns
|
|
392
|
+
if (output.includes('P1001') || output.includes('Can\'t reach database server')) {
|
|
393
|
+
resolve({
|
|
394
|
+
migrated: false,
|
|
395
|
+
error: 'Cannot connect to database to check migrations'
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (output.includes('P1003') || output.includes('does not exist')) {
|
|
401
|
+
resolve({
|
|
402
|
+
migrated: false,
|
|
403
|
+
error: 'Database does not exist',
|
|
404
|
+
needsSetup: true
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check for "Database schema is not empty" - tables exist but no migrations
|
|
410
|
+
if (output.includes('Database schema is not empty')) {
|
|
411
|
+
resolve({
|
|
412
|
+
migrated: false,
|
|
413
|
+
error: 'Database has tables but no migration history. Run prisma migrate to baseline.',
|
|
414
|
+
needsBaseline: true
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check for pending migrations
|
|
420
|
+
if (output.includes('Following migration') && output.includes('have not yet been applied')) {
|
|
421
|
+
const pendingMatch = output.match(/Following migration[s]? have not yet been applied:\s*([\s\S]*?)(?:To apply|$)/);
|
|
422
|
+
const pendingMigrations = pendingMatch
|
|
423
|
+
? pendingMatch[1].trim().split('\n').map(m => m.trim()).filter(Boolean)
|
|
424
|
+
: [];
|
|
425
|
+
|
|
426
|
+
resolve({
|
|
427
|
+
migrated: false,
|
|
428
|
+
pendingMigrations,
|
|
429
|
+
error: 'Database has pending migrations'
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check for "no migration found" - empty migrations folder
|
|
435
|
+
if (output.includes('No migration found') || output.includes('Database schema is up to date')) {
|
|
436
|
+
resolve({ migrated: true });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Check for table not found errors (P2021)
|
|
441
|
+
if (output.includes('P2021') || output.includes('does not exist in the current database')) {
|
|
442
|
+
resolve({
|
|
443
|
+
migrated: false,
|
|
444
|
+
error: 'Required database tables do not exist. Run migrations first.',
|
|
445
|
+
needsSetup: true
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// If exit code is 0 and no error patterns, assume migrated
|
|
451
|
+
if (code === 0) {
|
|
452
|
+
resolve({ migrated: true });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Unknown error
|
|
457
|
+
resolve({
|
|
458
|
+
migrated: false,
|
|
459
|
+
error: output.trim() || `prisma migrate status exited with code ${code}`
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Timeout after 30 seconds
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
child.kill();
|
|
466
|
+
resolve({
|
|
467
|
+
migrated: false,
|
|
468
|
+
error: 'Timeout checking migration status'
|
|
469
|
+
});
|
|
470
|
+
}, 30000);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Run Prisma migrations
|
|
476
|
+
* @param {string} projectPath - Path to the project
|
|
477
|
+
* @param {object} options - Options
|
|
478
|
+
* @param {boolean} options.dev - Use dev mode (interactive, can create migrations)
|
|
479
|
+
* @returns {Promise<{success: boolean, output?: string, error?: string}>}
|
|
480
|
+
*/
|
|
481
|
+
async runMigrations(projectPath, options = {}) {
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
const fs = require('fs');
|
|
484
|
+
|
|
485
|
+
// Look for prisma schema
|
|
486
|
+
// Frigg apps use the schema from @friggframework/core
|
|
487
|
+
const possibleSchemaPaths = [
|
|
488
|
+
// Project-local prisma schemas
|
|
489
|
+
path.join(projectPath, 'prisma', 'postgresql', 'schema.prisma'),
|
|
490
|
+
path.join(projectPath, 'prisma', 'schema.prisma'),
|
|
491
|
+
// Frigg core schemas (in node_modules)
|
|
492
|
+
path.join(projectPath, 'node_modules', '@friggframework', 'core', 'prisma-postgresql', 'schema.prisma'),
|
|
493
|
+
path.join(projectPath, 'node_modules', '@friggframework', 'core', 'generated', 'prisma-postgresql', 'schema.prisma')
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
let schemaPath = null;
|
|
497
|
+
for (const p of possibleSchemaPaths) {
|
|
498
|
+
if (fs.existsSync(p)) {
|
|
499
|
+
schemaPath = p;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!schemaPath) {
|
|
505
|
+
resolve({ success: false, error: 'No Prisma schema found' });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Find prisma binary - check local node_modules first
|
|
510
|
+
const possiblePrismaPaths = [
|
|
511
|
+
path.join(projectPath, 'node_modules', '.bin', 'prisma'),
|
|
512
|
+
path.join(projectPath, 'node_modules', 'prisma', 'build', 'index.js')
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
let prismaBin = 'npx';
|
|
516
|
+
// Default to 'deploy' mode - non-interactive, just applies existing migrations
|
|
517
|
+
// Use 'dev' only if explicitly requested (which requires interactive terminal)
|
|
518
|
+
const migrateCommand = options.dev ? 'dev' : 'deploy';
|
|
519
|
+
let args = ['prisma', 'migrate', migrateCommand, '--schema', schemaPath];
|
|
520
|
+
|
|
521
|
+
// Try to find local prisma binary for pnpm/yarn workspaces
|
|
522
|
+
for (const p of possiblePrismaPaths) {
|
|
523
|
+
if (fs.existsSync(p)) {
|
|
524
|
+
if (p.endsWith('.js')) {
|
|
525
|
+
prismaBin = 'node';
|
|
526
|
+
args = [p, 'migrate', migrateCommand, '--schema', schemaPath];
|
|
527
|
+
} else {
|
|
528
|
+
prismaBin = p;
|
|
529
|
+
args = ['migrate', migrateCommand, '--schema', schemaPath];
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log(` Executing: ${prismaBin} ${args.join(' ')}`);
|
|
536
|
+
|
|
537
|
+
const child = spawn(prismaBin, args, {
|
|
538
|
+
cwd: projectPath,
|
|
539
|
+
env: { ...process.env },
|
|
540
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
541
|
+
shell: prismaBin === 'npx'
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
let stdout = '';
|
|
545
|
+
let stderr = '';
|
|
546
|
+
|
|
547
|
+
child.stdout.on('data', (data) => {
|
|
548
|
+
const text = data.toString();
|
|
549
|
+
stdout += text;
|
|
550
|
+
// Log progress in real-time
|
|
551
|
+
process.stdout.write(text);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
child.stderr.on('data', (data) => {
|
|
555
|
+
const text = data.toString();
|
|
556
|
+
stderr += text;
|
|
557
|
+
// Log errors in real-time
|
|
558
|
+
process.stderr.write(text);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
child.on('error', (error) => {
|
|
562
|
+
resolve({
|
|
563
|
+
success: false,
|
|
564
|
+
error: `Failed to run migrations: ${error.message}`
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
child.on('close', (code) => {
|
|
569
|
+
if (code === 0) {
|
|
570
|
+
resolve({ success: true, output: stdout });
|
|
571
|
+
} else {
|
|
572
|
+
resolve({
|
|
573
|
+
success: false,
|
|
574
|
+
error: stderr || stdout || `Migration failed with exit code ${code}`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Timeout after 60 seconds for migrations (deploy mode is fast)
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
child.kill();
|
|
582
|
+
resolve({
|
|
583
|
+
success: false,
|
|
584
|
+
error: 'Timeout running migrations (60s)'
|
|
585
|
+
});
|
|
586
|
+
}, 60000);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
module.exports = { DatabaseAdapter };
|