@friggframework/core 2.0.0--canary.461.5ebf96a.0 → 2.0.0--canary.461.6096bdb.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.
@@ -201,6 +201,15 @@ async function disconnectPrisma() {
201
201
 
202
202
  async function connectPrisma() {
203
203
  await getPrismaClient().$connect();
204
+
205
+ // Initialize MongoDB schema - ensure all collections exist
206
+ // Only run for MongoDB/DocumentDB (not PostgreSQL)
207
+ // This prevents "Cannot create namespace in multi-document transaction" errors
208
+ if (config.DB_TYPE === 'mongodb') {
209
+ const { initializeMongoDBSchema } = require('./utils/mongodb-schema-init');
210
+ await initializeMongoDBSchema();
211
+ }
212
+
204
213
  return getPrismaClient();
205
214
  }
206
215
 
@@ -38,6 +38,9 @@ class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
38
38
  }
39
39
 
40
40
  async createCredential(credentialData) {
41
+ // Note: Collection existence is ensured at application startup via
42
+ // initializeMongoDBSchema() in database/utils/mongodb-schema-init.js
43
+ // This prevents "Cannot create namespace in multi-document transaction" errors
41
44
  return await prisma.credential.create({
42
45
  data: credentialData,
43
46
  });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * MongoDB Collection Utilities
3
+ *
4
+ * Provides utilities for managing MongoDB collections, particularly for
5
+ * handling the constraint that collections cannot be created inside
6
+ * multi-document transactions.
7
+ *
8
+ * @see https://github.com/prisma/prisma/issues/8305
9
+ * @see https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-operations
10
+ */
11
+
12
+ const { mongoose } = require('../mongoose');
13
+
14
+ /**
15
+ * Ensures a MongoDB collection exists
16
+ *
17
+ * MongoDB doesn't allow creating collections (namespaces) inside multi-document
18
+ * transactions. This function checks if a collection exists and creates it if needed,
19
+ * preventing "Cannot create namespace in multi-document transaction" errors.
20
+ *
21
+ * @param {string} collectionName - Name of the collection to ensure exists
22
+ * @returns {Promise<void>}
23
+ *
24
+ * @example
25
+ * ```js
26
+ * await ensureCollectionExists('Credential');
27
+ * // Now safe to create documents in Credential collection
28
+ * await prisma.credential.create({ data: {...} });
29
+ * ```
30
+ */
31
+ async function ensureCollectionExists(collectionName) {
32
+ try {
33
+ const collections = await mongoose.connection.db
34
+ .listCollections({ name: collectionName })
35
+ .toArray();
36
+
37
+ if (collections.length === 0) {
38
+ // Collection doesn't exist, create it outside of any transaction
39
+ await mongoose.connection.db.createCollection(collectionName);
40
+ console.log(`Created MongoDB collection: ${collectionName}`);
41
+ }
42
+ } catch (error) {
43
+ // Collection might already exist due to race condition, or other error
44
+ // Log warning but don't fail - let subsequent operations handle errors
45
+ if (error.codeName === 'NamespaceExists') {
46
+ // This is expected in race conditions, silently continue
47
+ return;
48
+ }
49
+ console.warn(`Error ensuring collection ${collectionName} exists:`, error.message);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Ensures multiple MongoDB collections exist
55
+ *
56
+ * @param {string[]} collectionNames - Array of collection names to ensure exist
57
+ * @returns {Promise<void>}
58
+ *
59
+ * @example
60
+ * ```js
61
+ * await ensureCollectionsExist(['Credential', 'User', 'Token']);
62
+ * ```
63
+ */
64
+ async function ensureCollectionsExist(collectionNames) {
65
+ await Promise.all(collectionNames.map(name => ensureCollectionExists(name)));
66
+ }
67
+
68
+ /**
69
+ * Checks if a collection exists in MongoDB
70
+ *
71
+ * @param {string} collectionName - Name of the collection to check
72
+ * @returns {Promise<boolean>} True if collection exists, false otherwise
73
+ */
74
+ async function collectionExists(collectionName) {
75
+ try {
76
+ const collections = await mongoose.connection.db
77
+ .listCollections({ name: collectionName })
78
+ .toArray();
79
+
80
+ return collections.length > 0;
81
+ } catch (error) {
82
+ console.error(`Error checking if collection ${collectionName} exists:`, error.message);
83
+ return false;
84
+ }
85
+ }
86
+
87
+ module.exports = {
88
+ ensureCollectionExists,
89
+ ensureCollectionsExist,
90
+ collectionExists,
91
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * MongoDB Schema Initialization for Prisma
3
+ *
4
+ * Dynamically parses the Prisma schema and ensures all collections exist before
5
+ * the application starts handling requests. This prevents
6
+ * "Cannot create namespace in multi-document transaction" errors.
7
+ *
8
+ * MongoDB does not allow creating collections inside transactions.
9
+ * By pre-creating all collections at startup, we ensure all Prisma
10
+ * operations can safely use transactions without namespace creation errors.
11
+ *
12
+ * Collection names are extracted dynamically from the Prisma schema file,
13
+ * ensuring they stay in sync with schema changes without manual updates.
14
+ *
15
+ * @see https://github.com/prisma/prisma/issues/8305
16
+ * @see https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-operations
17
+ */
18
+
19
+ const { mongoose } = require('../mongoose');
20
+ const { ensureCollectionsExist } = require('./mongodb-collection-utils');
21
+ const { getCollectionsFromSchemaSync } = require('./prisma-schema-parser');
22
+ const config = require('../config');
23
+
24
+ /**
25
+ * Initialize MongoDB schema by ensuring all collections exist
26
+ *
27
+ * This should be called once at application startup, after the database
28
+ * connection is established but before handling any requests.
29
+ *
30
+ * Dynamically parses the Prisma schema to extract collection names,
31
+ * ensuring automatic sync with schema changes.
32
+ *
33
+ * Benefits:
34
+ * - Prevents transaction namespace creation errors
35
+ * - Fails fast if there are database connection issues
36
+ * - Ensures consistent state across all instances
37
+ * - Idempotent - safe to run multiple times
38
+ * - Automatically syncs with Prisma schema changes
39
+ *
40
+ * @returns {Promise<void>}
41
+ *
42
+ * @example
43
+ * ```js
44
+ * await connectPrisma();
45
+ * await initializeMongoDBSchema(); // Run after connection
46
+ * // Now safe to handle requests
47
+ * ```
48
+ */
49
+ async function initializeMongoDBSchema() {
50
+ // Only run for MongoDB
51
+ if (config.DB_TYPE !== 'mongodb') {
52
+ console.log('Schema initialization skipped - not using MongoDB');
53
+ return;
54
+ }
55
+
56
+ // Check if database is connected
57
+ if (mongoose.connection.readyState !== 1) {
58
+ throw new Error(
59
+ 'Cannot initialize MongoDB schema - database not connected. ' +
60
+ 'Call connectPrisma() before initializeMongoDBSchema()'
61
+ );
62
+ }
63
+
64
+ console.log('Initializing MongoDB schema - ensuring all collections exist...');
65
+ const startTime = Date.now();
66
+
67
+ try {
68
+ // Dynamically parse Prisma schema to get collection names
69
+ const collections = getCollectionsFromSchemaSync();
70
+
71
+ if (collections.length === 0) {
72
+ console.warn('No collections found in Prisma schema - skipping initialization');
73
+ return;
74
+ }
75
+
76
+ await ensureCollectionsExist(collections);
77
+
78
+ const duration = Date.now() - startTime;
79
+ console.log(
80
+ `MongoDB schema initialization complete - ${collections.length} collections verified (${duration}ms)`
81
+ );
82
+ } catch (error) {
83
+ console.error('Failed to initialize MongoDB schema:', error.message);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get list of Prisma collection names by parsing the schema
90
+ * Useful for testing and introspection
91
+ *
92
+ * @returns {string[]} Array of collection names from Prisma schema
93
+ */
94
+ function getPrismaCollections() {
95
+ try {
96
+ return getCollectionsFromSchemaSync();
97
+ } catch (error) {
98
+ console.warn('Could not parse Prisma collections:', error.message);
99
+ return [];
100
+ }
101
+ }
102
+
103
+ module.exports = {
104
+ initializeMongoDBSchema,
105
+ getPrismaCollections,
106
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Prisma Schema Parser for MongoDB Collections
3
+ *
4
+ * Dynamically parses the Prisma schema file to extract MongoDB collection names.
5
+ * This ensures collection names stay in sync with the schema without hardcoding.
6
+ *
7
+ * Handles:
8
+ * - @@map() directives (custom collection names)
9
+ * - Models without @@map() (uses model name)
10
+ * - Comments and whitespace
11
+ * - Multiple schema file locations
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ /**
18
+ * Parse Prisma schema file to extract collection names
19
+ *
20
+ * Reads the schema.prisma file and extracts all model definitions,
21
+ * returning the actual MongoDB collection names (from @@map directives).
22
+ *
23
+ * @param {string} schemaPath - Path to schema.prisma file
24
+ * @returns {Promise<string[]>} Array of collection names
25
+ *
26
+ * @example
27
+ * ```js
28
+ * const collections = await parseCollectionsFromSchema('./prisma/schema.prisma');
29
+ * // Returns: ['User', 'Token', 'Credential', ...]
30
+ * ```
31
+ */
32
+ async function parseCollectionsFromSchema(schemaPath) {
33
+ try {
34
+ const schemaContent = await fs.promises.readFile(schemaPath, 'utf-8');
35
+ return extractCollectionNames(schemaContent);
36
+ } catch (error) {
37
+ throw new Error(
38
+ `Failed to parse Prisma schema at ${schemaPath}: ${error.message}`
39
+ );
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Synchronous version of parseCollectionsFromSchema
45
+ *
46
+ * @param {string} schemaPath - Path to schema.prisma file
47
+ * @returns {string[]} Array of collection names
48
+ */
49
+ function parseCollectionsFromSchemaSync(schemaPath) {
50
+ try {
51
+ const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
52
+ return extractCollectionNames(schemaContent);
53
+ } catch (error) {
54
+ throw new Error(
55
+ `Failed to parse Prisma schema at ${schemaPath}: ${error.message}`
56
+ );
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Extract collection names from Prisma schema content
62
+ *
63
+ * Parses the schema content to find:
64
+ * 1. All model definitions
65
+ * 2. Their @@map() directives (if present)
66
+ * 3. Falls back to model name if no @@map()
67
+ *
68
+ * @param {string} schemaContent - Content of schema.prisma file
69
+ * @returns {string[]} Array of collection names
70
+ * @private
71
+ */
72
+ function extractCollectionNames(schemaContent) {
73
+ const collections = [];
74
+
75
+ // Match model blocks: "model ModelName { ... }"
76
+ // Using non-greedy match to handle multiple models
77
+ const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
78
+
79
+ let match;
80
+ while ((match = modelRegex.exec(schemaContent)) !== null) {
81
+ const modelName = match[1];
82
+ const modelBody = match[2];
83
+
84
+ // Look for @@map("CollectionName") directive
85
+ const mapMatch = modelBody.match(/@@map\s*\(\s*["'](\w+)["']\s*\)/);
86
+
87
+ if (mapMatch) {
88
+ // Use mapped collection name
89
+ collections.push(mapMatch[1]);
90
+ } else {
91
+ // Use model name as collection name (Prisma default)
92
+ collections.push(modelName);
93
+ }
94
+ }
95
+
96
+ return collections;
97
+ }
98
+
99
+ /**
100
+ * Find Prisma MongoDB schema file
101
+ *
102
+ * Searches for the schema.prisma file in common locations:
103
+ * 1. prisma-mongodb/schema.prisma (Frigg convention)
104
+ * 2. prisma/schema.prisma (Prisma default)
105
+ * 3. schema.prisma (root)
106
+ *
107
+ * @param {string} startDir - Directory to start searching from
108
+ * @returns {string|null} Path to schema file, or null if not found
109
+ */
110
+ function findMongoDBSchemaFile(startDir = __dirname) {
111
+ // Start from database directory and work up
112
+ const baseDir = path.resolve(startDir, '../..');
113
+
114
+ const searchPaths = [
115
+ path.join(baseDir, 'prisma-mongodb', 'schema.prisma'),
116
+ path.join(baseDir, 'prisma', 'schema.prisma'),
117
+ path.join(baseDir, 'schema.prisma'),
118
+ ];
119
+
120
+ for (const schemaPath of searchPaths) {
121
+ if (fs.existsSync(schemaPath)) {
122
+ return schemaPath;
123
+ }
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Get MongoDB collection names from Prisma schema
131
+ *
132
+ * Convenience function that finds and parses the schema automatically.
133
+ *
134
+ * @returns {Promise<string[]>} Array of collection names
135
+ * @throws {Error} If schema file not found or parsing fails
136
+ *
137
+ * @example
138
+ * ```js
139
+ * const collections = await getCollectionsFromSchema();
140
+ * await ensureCollectionsExist(collections);
141
+ * ```
142
+ */
143
+ async function getCollectionsFromSchema() {
144
+ const schemaPath = findMongoDBSchemaFile();
145
+
146
+ if (!schemaPath) {
147
+ throw new Error(
148
+ 'Could not find Prisma MongoDB schema file. ' +
149
+ 'Searched: prisma-mongodb/schema.prisma, prisma/schema.prisma, schema.prisma'
150
+ );
151
+ }
152
+
153
+ return await parseCollectionsFromSchema(schemaPath);
154
+ }
155
+
156
+ /**
157
+ * Synchronous version of getCollectionsFromSchema
158
+ *
159
+ * @returns {string[]} Array of collection names
160
+ * @throws {Error} If schema file not found or parsing fails
161
+ */
162
+ function getCollectionsFromSchemaSync() {
163
+ const schemaPath = findMongoDBSchemaFile();
164
+
165
+ if (!schemaPath) {
166
+ throw new Error(
167
+ 'Could not find Prisma MongoDB schema file. ' +
168
+ 'Searched: prisma-mongodb/schema.prisma, prisma/schema.prisma, schema.prisma'
169
+ );
170
+ }
171
+
172
+ return parseCollectionsFromSchemaSync(schemaPath);
173
+ }
174
+
175
+ module.exports = {
176
+ parseCollectionsFromSchema,
177
+ parseCollectionsFromSchemaSync,
178
+ extractCollectionNames,
179
+ findMongoDBSchemaFile,
180
+ getCollectionsFromSchema,
181
+ getCollectionsFromSchemaSync,
182
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.5ebf96a.0",
4
+ "version": "2.0.0--canary.461.6096bdb.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.461.5ebf96a.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.461.5ebf96a.0",
43
- "@friggframework/test": "2.0.0--canary.461.5ebf96a.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.461.6096bdb.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.461.6096bdb.0",
43
+ "@friggframework/test": "2.0.0--canary.461.6096bdb.0",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "5ebf96a4b08d18a165c8451176b92705a1dcbb4f"
83
+ "gitHead": "6096bdbabd54fc3f32a2fce0975467f69aaf273d"
84
84
  }