@friggframework/core 2.0.0-next.53 → 2.0.0-next.55
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/CLAUDE.md +2 -1
- package/application/commands/credential-commands.js +1 -1
- package/application/commands/integration-commands.js +1 -1
- package/application/index.js +1 -1
- package/core/create-handler.js +12 -0
- package/credential/repositories/credential-repository-documentdb.js +304 -0
- package/credential/repositories/credential-repository-factory.js +8 -1
- package/credential/repositories/credential-repository-mongo.js +16 -54
- package/credential/repositories/credential-repository-postgres.js +14 -41
- package/credential/use-cases/get-credential-for-user.js +7 -3
- package/database/config.js +4 -4
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +136 -0
- package/database/encryption/README.md +50 -1
- package/database/encryption/documentdb-encryption-service.md +3270 -0
- package/database/encryption/encryption-schema-registry.js +46 -0
- package/database/prisma.js +7 -47
- package/database/repositories/health-check-repository-documentdb.js +134 -0
- package/database/repositories/health-check-repository-factory.js +6 -1
- package/database/repositories/health-check-repository-interface.js +29 -34
- package/database/repositories/health-check-repository-mongodb.js +1 -3
- package/database/use-cases/check-database-state-use-case.js +3 -3
- package/database/use-cases/run-database-migration-use-case.js +6 -4
- package/database/use-cases/trigger-database-migration-use-case.js +2 -2
- package/database/utils/mongodb-schema-init.js +5 -5
- package/database/utils/prisma-runner.js +15 -9
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +2 -1
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/edge.js +3 -3
- package/generated/prisma-mongodb/index.d.ts +10 -4
- package/generated/prisma-mongodb/index.js +3 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +1 -3
- package/generated/prisma-mongodb/wasm.js +2 -2
- package/generated/prisma-postgresql/edge.js +3 -3
- package/generated/prisma-postgresql/index.d.ts +10 -4
- package/generated/prisma-postgresql/index.js +3 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +1 -3
- package/generated/prisma-postgresql/wasm.js +2 -2
- package/handlers/routers/db-migration.js +2 -3
- package/handlers/routers/health.js +0 -3
- package/handlers/workers/db-migration.js +8 -8
- package/integrations/integration-router.js +6 -6
- package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
- package/integrations/repositories/integration-repository-documentdb.js +210 -0
- package/integrations/repositories/integration-repository-factory.js +8 -1
- package/integrations/repositories/process-repository-documentdb.js +243 -0
- package/integrations/repositories/process-repository-factory.js +8 -1
- package/modules/repositories/module-repository-documentdb.js +307 -0
- package/modules/repositories/module-repository-factory.js +8 -1
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +1 -3
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
- package/prisma-postgresql/schema.prisma +1 -3
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +6 -1
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +8 -1
- package/token/repositories/token-repository-mongo.js +10 -3
- package/token/repositories/token-repository-postgres.js +10 -3
- package/user/repositories/user-repository-documentdb.js +432 -0
- package/user/repositories/user-repository-factory.js +6 -1
- package/user/repositories/user-repository-mongo.js +3 -2
- package/user/repositories/user-repository-postgres.js +3 -2
- package/user/use-cases/login-user.js +1 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
|
@@ -109,6 +109,51 @@ function registerCustomSchema(schema) {
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Loads and registers custom encryption schema from appDefinition.
|
|
114
|
+
* Gracefully handles cases where appDefinition is not available.
|
|
115
|
+
*
|
|
116
|
+
* This ensures that custom encryption schemas defined in the backend's index.js
|
|
117
|
+
* are registered before any repositories attempt to encrypt data.
|
|
118
|
+
*
|
|
119
|
+
* Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
|
|
120
|
+
*/
|
|
121
|
+
function loadCustomEncryptionSchema() {
|
|
122
|
+
try {
|
|
123
|
+
// Lazy require to avoid circular dependency issues
|
|
124
|
+
const path = require('node:path');
|
|
125
|
+
const { findNearestBackendPackageJson } = require('../../utils');
|
|
126
|
+
|
|
127
|
+
const backendPackagePath = findNearestBackendPackageJson();
|
|
128
|
+
if (!backendPackagePath) {
|
|
129
|
+
return; // No backend found, skip custom schema
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const backendDir = path.dirname(backendPackagePath);
|
|
133
|
+
const backendIndexPath = path.join(backendDir, 'index.js');
|
|
134
|
+
|
|
135
|
+
const backendModule = require(backendIndexPath);
|
|
136
|
+
const appDefinition = backendModule?.Definition;
|
|
137
|
+
|
|
138
|
+
if (!appDefinition) {
|
|
139
|
+
return; // No app definition found
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const customSchema = appDefinition.encryption?.schema;
|
|
143
|
+
|
|
144
|
+
if (customSchema && Object.keys(customSchema).length > 0) {
|
|
145
|
+
registerCustomSchema(customSchema);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Silently ignore errors - custom schema is optional
|
|
149
|
+
// This handles cases like:
|
|
150
|
+
// - Backend package.json not found (tests, standalone usage)
|
|
151
|
+
// - No appDefinition defined
|
|
152
|
+
// - No custom encryption schema specified
|
|
153
|
+
logger.debug('Could not load custom encryption schema:', error.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
112
157
|
function getEncryptedFields(modelName) {
|
|
113
158
|
const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
|
|
114
159
|
const customFields = customSchema[modelName]?.fields || [];
|
|
@@ -136,6 +181,7 @@ module.exports = {
|
|
|
136
181
|
hasEncryptedFields,
|
|
137
182
|
getEncryptedModels,
|
|
138
183
|
registerCustomSchema,
|
|
184
|
+
loadCustomEncryptionSchema,
|
|
139
185
|
validateCustomSchema,
|
|
140
186
|
resetCustomSchema, // For testing only
|
|
141
187
|
};
|
package/database/prisma.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createEncryptionExtension,
|
|
3
3
|
} = require('./encryption/prisma-encryption-extension');
|
|
4
|
-
const {
|
|
4
|
+
const { loadCustomEncryptionSchema } = require('./encryption/encryption-schema-registry');
|
|
5
5
|
const { logger } = require('./encryption/logger');
|
|
6
6
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
7
7
|
const config = require('./config');
|
|
@@ -11,7 +11,7 @@ const config = require('./config');
|
|
|
11
11
|
* Falls back to MONGO_URI if DATABASE_URL is not set
|
|
12
12
|
* Infrastructure layer concern - maps legacy MONGO_URI to Prisma's expected DATABASE_URL
|
|
13
13
|
*
|
|
14
|
-
* Note: This should only be called when DB_TYPE is 'mongodb'
|
|
14
|
+
* Note: This should only be called when DB_TYPE is 'mongodb' or 'documentdb'
|
|
15
15
|
*/
|
|
16
16
|
function ensureMongoDbUrl() {
|
|
17
17
|
// If DATABASE_URL is already set, use it
|
|
@@ -22,13 +22,13 @@ function ensureMongoDbUrl() {
|
|
|
22
22
|
// Fallback to MONGO_URI for backwards compatibility with DocumentDB deployments
|
|
23
23
|
if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
|
|
24
24
|
process.env.DATABASE_URL = process.env.MONGO_URI;
|
|
25
|
-
logger.debug('Using MONGO_URI as DATABASE_URL for
|
|
25
|
+
logger.debug('Using MONGO_URI as DATABASE_URL for Mongo-compatible connection');
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Neither is set - error
|
|
30
30
|
throw new Error(
|
|
31
|
-
'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB'
|
|
31
|
+
'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB/DocumentDB'
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -59,46 +59,6 @@ function getEncryptionConfig() {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Loads and registers custom encryption schema from appDefinition
|
|
64
|
-
* Gracefully handles cases where appDefinition is not available
|
|
65
|
-
*/
|
|
66
|
-
function loadCustomEncryptionSchema() {
|
|
67
|
-
try {
|
|
68
|
-
// Lazy require to avoid circular dependency issues
|
|
69
|
-
const path = require('node:path');
|
|
70
|
-
const { findNearestBackendPackageJson } = require('../utils');
|
|
71
|
-
|
|
72
|
-
const backendPackagePath = findNearestBackendPackageJson();
|
|
73
|
-
if (!backendPackagePath) {
|
|
74
|
-
return; // No backend found, skip custom schema
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const backendDir = path.dirname(backendPackagePath);
|
|
78
|
-
const backendIndexPath = path.join(backendDir, 'index.js');
|
|
79
|
-
|
|
80
|
-
const backendModule = require(backendIndexPath);
|
|
81
|
-
const appDefinition = backendModule?.Definition;
|
|
82
|
-
|
|
83
|
-
if (!appDefinition) {
|
|
84
|
-
return; // No app definition found
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const customSchema = appDefinition.encryption?.schema;
|
|
88
|
-
|
|
89
|
-
if (customSchema && Object.keys(customSchema).length > 0) {
|
|
90
|
-
registerCustomSchema(customSchema);
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
// Silently ignore errors - custom schema is optional
|
|
94
|
-
// This handles cases like:
|
|
95
|
-
// - Backend package.json not found (tests, standalone usage)
|
|
96
|
-
// - No appDefinition defined
|
|
97
|
-
// - No custom encryption schema specified
|
|
98
|
-
logger.debug('Could not load custom encryption schema:', error.message);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
62
|
const prismaClientSingleton = () => {
|
|
103
63
|
let PrismaClient;
|
|
104
64
|
|
|
@@ -124,7 +84,7 @@ const prismaClientSingleton = () => {
|
|
|
124
84
|
);
|
|
125
85
|
};
|
|
126
86
|
|
|
127
|
-
if (config.DB_TYPE === 'mongodb') {
|
|
87
|
+
if (config.DB_TYPE === 'mongodb' || config.DB_TYPE === 'documentdb') {
|
|
128
88
|
// Ensure DATABASE_URL is set (fallback to MONGO_URI if needed)
|
|
129
89
|
ensureMongoDbUrl();
|
|
130
90
|
PrismaClient = loadPrismaClient('mongodb');
|
|
@@ -132,7 +92,7 @@ const prismaClientSingleton = () => {
|
|
|
132
92
|
PrismaClient = loadPrismaClient('postgresql');
|
|
133
93
|
} else {
|
|
134
94
|
throw new Error(
|
|
135
|
-
`Unsupported database type: ${config.DB_TYPE}. Supported values: 'mongodb', 'postgresql'`
|
|
95
|
+
`Unsupported database type: ${config.DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
136
96
|
);
|
|
137
97
|
}
|
|
138
98
|
|
|
@@ -205,7 +165,7 @@ async function connectPrisma() {
|
|
|
205
165
|
// Initialize MongoDB schema - ensure all collections exist
|
|
206
166
|
// Only run for MongoDB/DocumentDB (not PostgreSQL)
|
|
207
167
|
// This prevents "Cannot create namespace in multi-document transaction" errors
|
|
208
|
-
if (config.DB_TYPE === 'mongodb') {
|
|
168
|
+
if (config.DB_TYPE === 'mongodb' || config.DB_TYPE === 'documentdb') {
|
|
209
169
|
const { initializeMongoDBSchema } = require('./utils/mongodb-schema-init');
|
|
210
170
|
await initializeMongoDBSchema();
|
|
211
171
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const {
|
|
2
|
+
HealthCheckRepositoryInterface,
|
|
3
|
+
} = require('./health-check-repository-interface');
|
|
4
|
+
const {
|
|
5
|
+
toObjectId,
|
|
6
|
+
fromObjectId,
|
|
7
|
+
findOne,
|
|
8
|
+
insertOne,
|
|
9
|
+
deleteOne,
|
|
10
|
+
} = require('../documentdb-utils');
|
|
11
|
+
const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
|
|
12
|
+
|
|
13
|
+
class HealthCheckRepositoryDocumentDB extends HealthCheckRepositoryInterface {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} params
|
|
16
|
+
* @param {Object} params.prismaClient - Prisma client instance
|
|
17
|
+
*/
|
|
18
|
+
constructor({ prismaClient }) {
|
|
19
|
+
super();
|
|
20
|
+
this.prisma = prismaClient;
|
|
21
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @returns {Promise<{readyState: number, stateName: string, isConnected: boolean}>}
|
|
26
|
+
*/
|
|
27
|
+
async getDatabaseConnectionState() {
|
|
28
|
+
let isConnected = false;
|
|
29
|
+
let stateName = 'unknown';
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await this.prisma.$runCommandRaw({ ping: 1 });
|
|
33
|
+
isConnected = true;
|
|
34
|
+
stateName = 'connected';
|
|
35
|
+
} catch (error) {
|
|
36
|
+
stateName = 'disconnected';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
readyState: isConnected ? 1 : 0,
|
|
41
|
+
stateName,
|
|
42
|
+
isConnected,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {number} maxTimeMS
|
|
48
|
+
* @returns {Promise<number>} Response time in milliseconds
|
|
49
|
+
*/
|
|
50
|
+
async pingDatabase(maxTimeMS = 2000) {
|
|
51
|
+
const pingStart = Date.now();
|
|
52
|
+
|
|
53
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
54
|
+
setTimeout(() => reject(new Error('Database ping timeout')), maxTimeMS)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await Promise.race([
|
|
58
|
+
this.prisma.$runCommandRaw({ ping: 1 }),
|
|
59
|
+
timeoutPromise,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
return Date.now() - pingStart;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async createCredential(credentialData) {
|
|
66
|
+
const now = new Date();
|
|
67
|
+
const document = {
|
|
68
|
+
...credentialData,
|
|
69
|
+
createdAt: now,
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Encrypt sensitive fields before insert
|
|
74
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
75
|
+
'Credential',
|
|
76
|
+
document
|
|
77
|
+
);
|
|
78
|
+
const insertedId = await insertOne(this.prisma, 'Credential', encryptedDocument);
|
|
79
|
+
const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
|
|
80
|
+
|
|
81
|
+
// Decrypt after read
|
|
82
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
83
|
+
'Credential',
|
|
84
|
+
created
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: fromObjectId(decrypted._id),
|
|
89
|
+
...decrypted,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async findCredentialById(id) {
|
|
94
|
+
const doc = await findOne(this.prisma, 'Credential', {
|
|
95
|
+
_id: toObjectId(id),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!doc) return null;
|
|
99
|
+
|
|
100
|
+
// Decrypt sensitive fields
|
|
101
|
+
const decrypted = await this.encryptionService.decryptFields('Credential', doc);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id: fromObjectId(decrypted._id),
|
|
105
|
+
...decrypted,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getRawCredentialById(id) {
|
|
110
|
+
const objectId = toObjectId(id);
|
|
111
|
+
if (!objectId) return null;
|
|
112
|
+
|
|
113
|
+
const result = await this.prisma.$runCommandRaw({
|
|
114
|
+
find: 'Credential',
|
|
115
|
+
filter: { _id: objectId },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Return raw document WITHOUT decryption
|
|
119
|
+
// This allows the test to verify that fields are actually encrypted in the database
|
|
120
|
+
return result?.cursor?.firstBatch?.[0] ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async deleteCredential(id) {
|
|
124
|
+
const objectId = toObjectId(id);
|
|
125
|
+
if (!objectId) return false;
|
|
126
|
+
|
|
127
|
+
const result = await deleteOne(this.prisma, 'Credential', { _id: objectId });
|
|
128
|
+
const deleted = result?.n ?? 0;
|
|
129
|
+
return deleted > 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { HealthCheckRepositoryDocumentDB };
|
|
134
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { HealthCheckRepositoryMongoDB } = require('./health-check-repository-mongodb');
|
|
2
2
|
const { HealthCheckRepositoryPostgreSQL } = require('./health-check-repository-postgres');
|
|
3
|
+
const { HealthCheckRepositoryDocumentDB } = require('./health-check-repository-documentdb');
|
|
3
4
|
const config = require('../config');
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -29,9 +30,12 @@ function createHealthCheckRepository({ prismaClient } = {}) {
|
|
|
29
30
|
case 'postgresql':
|
|
30
31
|
return new HealthCheckRepositoryPostgreSQL({ prismaClient });
|
|
31
32
|
|
|
33
|
+
case 'documentdb':
|
|
34
|
+
return new HealthCheckRepositoryDocumentDB({ prismaClient });
|
|
35
|
+
|
|
32
36
|
default:
|
|
33
37
|
throw new Error(
|
|
34
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
|
|
38
|
+
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
35
39
|
);
|
|
36
40
|
}
|
|
37
41
|
}
|
|
@@ -40,4 +44,5 @@ module.exports = {
|
|
|
40
44
|
createHealthCheckRepository,
|
|
41
45
|
HealthCheckRepositoryMongoDB,
|
|
42
46
|
HealthCheckRepositoryPostgreSQL,
|
|
47
|
+
HealthCheckRepositoryDocumentDB,
|
|
43
48
|
};
|
|
@@ -15,72 +15,67 @@
|
|
|
15
15
|
*/
|
|
16
16
|
class HealthCheckRepositoryInterface {
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* @param {number} maxTimeMS - Maximum time in milliseconds
|
|
21
|
-
* @returns {Promise<number>} Response time in milliseconds
|
|
18
|
+
* @returns {Promise<{readyState: number, stateName: string, isConnected: boolean}>}
|
|
22
19
|
* @abstract
|
|
23
20
|
*/
|
|
24
|
-
async
|
|
25
|
-
throw new Error('Method
|
|
21
|
+
async getDatabaseConnectionState() {
|
|
22
|
+
throw new Error('Method getDatabaseConnectionState must be implemented by subclass');
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
26
|
+
* Ping database to verify connectivity
|
|
30
27
|
*
|
|
31
|
-
* @param {
|
|
32
|
-
* @
|
|
33
|
-
* @returns {Promise<Object>} Saved document
|
|
28
|
+
* @param {number} maxTimeMS - Maximum time in milliseconds
|
|
29
|
+
* @returns {Promise<number>} Response time in milliseconds
|
|
34
30
|
* @abstract
|
|
35
31
|
*/
|
|
36
|
-
async
|
|
37
|
-
throw new Error('Method
|
|
32
|
+
async pingDatabase(maxTimeMS) {
|
|
33
|
+
throw new Error('Method pingDatabase must be implemented by subclass');
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
|
-
*
|
|
37
|
+
* Persist an encrypted credential for health verification.
|
|
38
|
+
* Implementations should rely on Prisma so encryption middleware runs.
|
|
42
39
|
*
|
|
43
|
-
* @param {Object}
|
|
44
|
-
* @
|
|
45
|
-
* @returns {Promise<Object|null>} Document or null
|
|
40
|
+
* @param {Object} credentialData
|
|
41
|
+
* @returns {Promise<Object>} Persisted credential
|
|
46
42
|
* @abstract
|
|
47
43
|
*/
|
|
48
|
-
async
|
|
49
|
-
throw new Error('Method
|
|
44
|
+
async createCredential(credentialData) {
|
|
45
|
+
throw new Error('Method createCredential must be implemented by subclass');
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
/**
|
|
53
|
-
*
|
|
49
|
+
* Retrieve credential by ID using Prisma (decrypted).
|
|
54
50
|
*
|
|
55
|
-
* @param {string}
|
|
56
|
-
* @
|
|
57
|
-
* @returns {Promise<Object|null>} Raw document or null
|
|
51
|
+
* @param {string} id
|
|
52
|
+
* @returns {Promise<Object|null>}
|
|
58
53
|
* @abstract
|
|
59
54
|
*/
|
|
60
|
-
async
|
|
61
|
-
throw new Error('Method
|
|
55
|
+
async findCredentialById(id) {
|
|
56
|
+
throw new Error('Method findCredentialById must be implemented by subclass');
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
/**
|
|
65
|
-
*
|
|
60
|
+
* Fetch raw credential document from the database (without decryption).
|
|
66
61
|
*
|
|
67
|
-
* @param {
|
|
68
|
-
* @
|
|
69
|
-
* @returns {Promise<Object>} Deletion result
|
|
62
|
+
* @param {string} id
|
|
63
|
+
* @returns {Promise<Object|null>}
|
|
70
64
|
* @abstract
|
|
71
65
|
*/
|
|
72
|
-
async
|
|
73
|
-
throw new Error('Method
|
|
66
|
+
async getRawCredentialById(id) {
|
|
67
|
+
throw new Error('Method getRawCredentialById must be implemented by subclass');
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
/**
|
|
77
|
-
*
|
|
71
|
+
* Delete credential by ID.
|
|
78
72
|
*
|
|
79
|
-
* @
|
|
73
|
+
* @param {string} id
|
|
74
|
+
* @returns {Promise<void>}
|
|
80
75
|
* @abstract
|
|
81
76
|
*/
|
|
82
|
-
async
|
|
83
|
-
throw new Error('Method
|
|
77
|
+
async deleteCredential(id) {
|
|
78
|
+
throw new Error('Method deleteCredential must be implemented by subclass');
|
|
84
79
|
}
|
|
85
80
|
}
|
|
86
81
|
|
|
@@ -32,7 +32,7 @@ class CheckDatabaseStateUseCase {
|
|
|
32
32
|
/**
|
|
33
33
|
* Execute check migration status
|
|
34
34
|
*
|
|
35
|
-
* @param {string} dbType - Database type (postgresql or
|
|
35
|
+
* @param {string} dbType - Database type (postgresql, mongodb, or documentdb)
|
|
36
36
|
* @param {string} stage - Deployment stage (default: 'production')
|
|
37
37
|
* @returns {Promise<Object>} Migration status
|
|
38
38
|
*/
|
|
@@ -42,8 +42,8 @@ class CheckDatabaseStateUseCase {
|
|
|
42
42
|
throw new ValidationError('dbType is required');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
if (!['postgresql', 'mongodb'].includes(dbType)) {
|
|
46
|
-
throw new ValidationError('dbType must be postgresql or
|
|
45
|
+
if (!['postgresql', 'mongodb', 'documentdb'].includes(dbType)) {
|
|
46
|
+
throw new ValidationError('dbType must be postgresql, mongodb, or documentdb');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
console.log(`Checking migration status for ${dbType} in ${stage}`);
|
|
@@ -26,7 +26,7 @@ class RunDatabaseMigrationUseCase {
|
|
|
26
26
|
* Execute database migration
|
|
27
27
|
*
|
|
28
28
|
* @param {Object} params
|
|
29
|
-
* @param {string} params.dbType - Database type ('postgresql' or '
|
|
29
|
+
* @param {string} params.dbType - Database type ('postgresql', 'mongodb', or 'documentdb')
|
|
30
30
|
* @param {string} params.stage - Deployment stage (determines migration command)
|
|
31
31
|
* @param {boolean} [params.verbose=false] - Enable verbose output
|
|
32
32
|
* @returns {Promise<Object>} Migration result { success, dbType, stage, command, message }
|
|
@@ -61,19 +61,21 @@ class RunDatabaseMigrationUseCase {
|
|
|
61
61
|
{ dbType, stage, command: migrationCommand, step: 'migrate', output: migrationResult.output }
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
|
-
} else if (dbType === 'mongodb') {
|
|
64
|
+
} else if (dbType === 'mongodb' || dbType === 'documentdb') {
|
|
65
65
|
migrationCommand = 'db push';
|
|
66
66
|
// Use non-interactive mode for automated/Lambda environments
|
|
67
67
|
migrationResult = await this.prismaRunner.runPrismaDbPush(verbose, true);
|
|
68
68
|
|
|
69
69
|
if (!migrationResult.success) {
|
|
70
70
|
throw new MigrationError(
|
|
71
|
-
`
|
|
71
|
+
`Mongo-compatible push failed: ${migrationResult.error || 'Unknown error'}`,
|
|
72
72
|
{ dbType, stage, command: migrationCommand, step: 'push', output: migrationResult.output }
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
} else {
|
|
76
|
-
throw new ValidationError(
|
|
76
|
+
throw new ValidationError(
|
|
77
|
+
`Unsupported database type: ${dbType}. Must be 'postgresql', 'mongodb', or 'documentdb'.`
|
|
78
|
+
);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// Return success result
|
|
@@ -38,7 +38,7 @@ class TriggerDatabaseMigrationUseCase {
|
|
|
38
38
|
*
|
|
39
39
|
* @param {Object} params
|
|
40
40
|
* @param {string} params.userId - User ID triggering the migration
|
|
41
|
-
* @param {string} params.dbType - Database type ('postgresql' or '
|
|
41
|
+
* @param {string} params.dbType - Database type ('postgresql', 'mongodb', or 'documentdb')
|
|
42
42
|
* @param {string} params.stage - Deployment stage (determines migration command)
|
|
43
43
|
* @returns {Promise<Object>} Process info { success, processId, state, statusUrl, message }
|
|
44
44
|
* @throws {ValidationError} If parameters are invalid
|
|
@@ -123,7 +123,7 @@ class TriggerDatabaseMigrationUseCase {
|
|
|
123
123
|
throw new ValidationError('dbType must be a string');
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const validDbTypes = ['postgresql', 'mongodb'];
|
|
126
|
+
const validDbTypes = ['postgresql', 'mongodb', 'documentdb'];
|
|
127
127
|
if (!validDbTypes.includes(dbType)) {
|
|
128
128
|
throw new ValidationError(
|
|
129
129
|
`Invalid dbType: "${dbType}". Must be one of: ${validDbTypes.join(', ')}`
|
|
@@ -47,9 +47,9 @@ const config = require('../config');
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
async function initializeMongoDBSchema() {
|
|
50
|
-
// Only run for MongoDB
|
|
51
|
-
if (config.DB_TYPE !== 'mongodb') {
|
|
52
|
-
console.log('Schema initialization skipped - not using MongoDB');
|
|
50
|
+
// Only run for MongoDB-compatible databases
|
|
51
|
+
if (config.DB_TYPE !== 'mongodb' && config.DB_TYPE !== 'documentdb') {
|
|
52
|
+
console.log('Schema initialization skipped - not using MongoDB-compatible database');
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -61,7 +61,7 @@ async function initializeMongoDBSchema() {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
console.log('Initializing MongoDB schema - ensuring all collections exist...');
|
|
64
|
+
console.log('Initializing MongoDB-compatible schema - ensuring all collections exist...');
|
|
65
65
|
const startTime = Date.now();
|
|
66
66
|
|
|
67
67
|
try {
|
|
@@ -77,7 +77,7 @@ async function initializeMongoDBSchema() {
|
|
|
77
77
|
|
|
78
78
|
const duration = Date.now() - startTime;
|
|
79
79
|
console.log(
|
|
80
|
-
`MongoDB schema initialization complete - ${collections.length} collections verified (${duration}ms)`
|
|
80
|
+
`MongoDB-compatible schema initialization complete - ${collections.length} collections verified (${duration}ms)`
|
|
81
81
|
);
|
|
82
82
|
} catch (error) {
|
|
83
83
|
console.error('Failed to initialize MongoDB schema:', error.message);
|
|
@@ -10,12 +10,17 @@ const chalk = require('chalk');
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Gets the path to the Prisma schema file for the database type
|
|
13
|
-
* @param {'mongodb'|'postgresql'} dbType - Database type
|
|
13
|
+
* @param {'mongodb'|'postgresql'|'documentdb'} dbType - Database type
|
|
14
14
|
* @param {string} projectRoot - Project root directory
|
|
15
15
|
* @returns {string} Absolute path to schema file
|
|
16
16
|
* @throws {Error} If schema file doesn't exist
|
|
17
17
|
*/
|
|
18
|
+
function normalizeMongoCompatible(dbType) {
|
|
19
|
+
return dbType === 'documentdb' ? 'mongodb' : dbType;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
function getPrismaSchemaPath(dbType, projectRoot = process.cwd()) {
|
|
23
|
+
const normalizedType = normalizeMongoCompatible(dbType);
|
|
19
24
|
// Try multiple locations for the schema file
|
|
20
25
|
// Priority order:
|
|
21
26
|
// 1. Lambda layer path (where the schema actually exists in deployed Lambda)
|
|
@@ -23,10 +28,10 @@ function getPrismaSchemaPath(dbType, projectRoot = process.cwd()) {
|
|
|
23
28
|
// 3. Parent node_modules (workspace/monorepo setup)
|
|
24
29
|
const possiblePaths = [
|
|
25
30
|
// Lambda layer path - this is where the schema actually exists in deployed Lambda
|
|
26
|
-
`/opt/nodejs/node_modules/generated/prisma-${
|
|
31
|
+
`/opt/nodejs/node_modules/generated/prisma-${normalizedType}/schema.prisma`,
|
|
27
32
|
// Check where Frigg is installed via npm (production scenario)
|
|
28
|
-
path.join(projectRoot, 'node_modules', '@friggframework', 'core', `prisma-${
|
|
29
|
-
path.join(projectRoot, '..', 'node_modules', '@friggframework', 'core', `prisma-${
|
|
33
|
+
path.join(projectRoot, 'node_modules', '@friggframework', 'core', `prisma-${normalizedType}`, 'schema.prisma'),
|
|
34
|
+
path.join(projectRoot, '..', 'node_modules', '@friggframework', 'core', `prisma-${normalizedType}`, 'schema.prisma')
|
|
30
35
|
];
|
|
31
36
|
|
|
32
37
|
for (const schemaPath of possiblePaths) {
|
|
@@ -44,7 +49,7 @@ function getPrismaSchemaPath(dbType, projectRoot = process.cwd()) {
|
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
51
|
* Runs prisma generate for the specified database type
|
|
47
|
-
* @param {'mongodb'|'postgresql'} dbType - Database type
|
|
52
|
+
* @param {'mongodb'|'postgresql'|'documentdb'} dbType - Database type
|
|
48
53
|
* @param {boolean} verbose - Enable verbose output
|
|
49
54
|
* @returns {Promise<Object>} { success: boolean, output?: string, error?: string }
|
|
50
55
|
*/
|
|
@@ -53,18 +58,19 @@ async function runPrismaGenerate(dbType, verbose = false) {
|
|
|
53
58
|
const schemaPath = getPrismaSchemaPath(dbType);
|
|
54
59
|
|
|
55
60
|
// Check if Prisma client already exists (e.g., in Lambda or pre-generated)
|
|
56
|
-
const
|
|
61
|
+
const normalizedType = normalizeMongoCompatible(dbType);
|
|
62
|
+
const generatedClientPath = path.join(path.dirname(path.dirname(schemaPath)), 'generated', `prisma-${normalizedType}`, 'client.js');
|
|
57
63
|
const isLambdaEnvironment = !!process.env.AWS_LAMBDA_FUNCTION_NAME || !!process.env.LAMBDA_TASK_ROOT;
|
|
58
64
|
|
|
59
65
|
// In Lambda, also check the layer path (/opt/nodejs/node_modules)
|
|
60
|
-
const lambdaLayerClientPath = `/opt/nodejs/node_modules/generated/prisma-${
|
|
66
|
+
const lambdaLayerClientPath = `/opt/nodejs/node_modules/generated/prisma-${normalizedType}/client.js`;
|
|
61
67
|
|
|
62
68
|
const clientExists = fs.existsSync(generatedClientPath) || (isLambdaEnvironment && fs.existsSync(lambdaLayerClientPath));
|
|
63
69
|
|
|
64
70
|
if (clientExists) {
|
|
65
71
|
const foundPath = fs.existsSync(generatedClientPath) ? generatedClientPath : lambdaLayerClientPath;
|
|
66
72
|
if (verbose) {
|
|
67
|
-
|
|
73
|
+
console.log(chalk.gray(`✓ Prisma client already generated at: ${foundPath}`));
|
|
68
74
|
}
|
|
69
75
|
if (isLambdaEnvironment) {
|
|
70
76
|
if (verbose) {
|
|
@@ -110,7 +116,7 @@ async function runPrismaGenerate(dbType, verbose = false) {
|
|
|
110
116
|
|
|
111
117
|
/**
|
|
112
118
|
* Checks database migration status
|
|
113
|
-
* @param {'mongodb'|'postgresql'} dbType - Database type
|
|
119
|
+
* @param {'mongodb'|'postgresql'|'documentdb'} dbType - Database type
|
|
114
120
|
* @returns {Promise<Object>} { upToDate: boolean, pendingMigrations?: number, error?: string }
|
|
115
121
|
*/
|
|
116
122
|
async function checkDatabaseState(dbType) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { BaseError } = require('./base-error');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClientSafeError - An error that is safe to expose to end users
|
|
5
|
+
*
|
|
6
|
+
* Use this error class when the error message does not contain sensitive
|
|
7
|
+
* implementation details and can be safely shown to users.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "Invalid Token: Token is expired"
|
|
11
|
+
* - "User not found"
|
|
12
|
+
* - "Invalid credentials"
|
|
13
|
+
*
|
|
14
|
+
* @param {string} message - The user-safe error message
|
|
15
|
+
* @param {number} statusCode - HTTP status code (default: 400)
|
|
16
|
+
* @param {object} options - Additional error options (cause, etc.)
|
|
17
|
+
*/
|
|
18
|
+
class ClientSafeError extends BaseError {
|
|
19
|
+
constructor(message, statusCode = 400, options) {
|
|
20
|
+
super(message, options);
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
this.isClientSafe = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { ClientSafeError };
|
package/errors/fetch-error.js
CHANGED