@tamyla/clodo-framework 3.0.7 → 3.0.9
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/CHANGELOG.md +15 -0
- package/bin/shared/cloudflare/ops.js +46 -4
- package/dist/database/database-orchestrator.js +25 -4
- package/dist/orchestration/multi-domain-orchestrator.js +49 -13
- package/dist/shared/cloudflare/ops.js +66 -4
- package/dist/utils/cloudflare/api.js +51 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [3.0.9](https://github.com/tamylaa/clodo-framework/compare/v3.0.8...v3.0.9) (2025-10-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Auto-create D1 databases before applying migrations ([bb4a780](https://github.com/tamylaa/clodo-framework/commit/bb4a7804307f4ee8ff17256287e61098d809b0e9))
|
|
7
|
+
|
|
8
|
+
## [3.0.8](https://github.com/tamylaa/clodo-framework/compare/v3.0.7...v3.0.8) (2025-10-14)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add --env flag to D1 migration commands for environment-specific database configs ([ef095e3](https://github.com/tamylaa/clodo-framework/commit/ef095e3ce7d8ea10e0e45d7eba45011cfa4271db))
|
|
14
|
+
* implement API token authentication for D1 database operations ([d8be0a9](https://github.com/tamylaa/clodo-framework/commit/d8be0a9388bf3ed5cf433fa8b61108acd25007fd))
|
|
15
|
+
|
|
1
16
|
## [3.0.7](https://github.com/tamylaa/clodo-framework/compare/v3.0.6...v3.0.7) (2025-10-14)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -227,7 +227,17 @@ export async function listSecrets(env = 'production') {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
export async function listDatabases() {
|
|
230
|
+
export async function listDatabases(options = {}) {
|
|
231
|
+
const { apiToken, accountId } = options;
|
|
232
|
+
|
|
233
|
+
// Use API-based operation if credentials provided
|
|
234
|
+
if (apiToken && accountId) {
|
|
235
|
+
const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
|
|
236
|
+
const cf = new CloudflareAPI(apiToken);
|
|
237
|
+
return await cf.listD1Databases(accountId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Fallback to CLI-based operation
|
|
231
241
|
try {
|
|
232
242
|
const { stdout: list } = await executeWithRateLimit('npx wrangler d1 list', 'd1');
|
|
233
243
|
return list;
|
|
@@ -236,7 +246,17 @@ export async function listDatabases() {
|
|
|
236
246
|
}
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
export async function databaseExists(databaseName) {
|
|
249
|
+
export async function databaseExists(databaseName, options = {}) {
|
|
250
|
+
const { apiToken, accountId } = options;
|
|
251
|
+
|
|
252
|
+
// Use API-based operation if credentials provided
|
|
253
|
+
if (apiToken && accountId) {
|
|
254
|
+
const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
|
|
255
|
+
const cf = new CloudflareAPI(apiToken);
|
|
256
|
+
return await cf.d1DatabaseExists(accountId, databaseName);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Fallback to CLI-based operation
|
|
240
260
|
try {
|
|
241
261
|
const list = await listDatabases();
|
|
242
262
|
return list.includes(databaseName);
|
|
@@ -245,7 +265,18 @@ export async function databaseExists(databaseName) {
|
|
|
245
265
|
}
|
|
246
266
|
}
|
|
247
267
|
|
|
248
|
-
export async function createDatabase(name) {
|
|
268
|
+
export async function createDatabase(name, options = {}) {
|
|
269
|
+
const { apiToken, accountId } = options;
|
|
270
|
+
|
|
271
|
+
// Use API-based operation if credentials provided
|
|
272
|
+
if (apiToken && accountId) {
|
|
273
|
+
const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
|
|
274
|
+
const cf = new CloudflareAPI(apiToken);
|
|
275
|
+
const result = await cf.createD1Database(accountId, name);
|
|
276
|
+
return result.uuid; // Return UUID to match CLI behavior
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Fallback to CLI-based operation
|
|
249
280
|
try {
|
|
250
281
|
const { stdout: output } = await executeWithRateLimit(`npx wrangler d1 create ${name}`, 'd1');
|
|
251
282
|
const idMatch = output.match(/database_id = "([^"]+)"/);
|
|
@@ -319,7 +350,18 @@ export async function executeSql(databaseName, sql, env = 'production') {
|
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
// Get database ID from list output
|
|
322
|
-
export async function getDatabaseId(databaseName) {
|
|
353
|
+
export async function getDatabaseId(databaseName, options = {}) {
|
|
354
|
+
const { apiToken, accountId } = options;
|
|
355
|
+
|
|
356
|
+
// Use API-based operation if credentials provided
|
|
357
|
+
if (apiToken && accountId) {
|
|
358
|
+
const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
|
|
359
|
+
const cf = new CloudflareAPI(apiToken);
|
|
360
|
+
const db = await cf.getD1Database(accountId, databaseName);
|
|
361
|
+
return db?.uuid || null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Fallback to CLI-based operation
|
|
323
365
|
try {
|
|
324
366
|
const list = await listDatabases();
|
|
325
367
|
const lines = list.split('\n');
|
|
@@ -39,6 +39,10 @@ export class DatabaseOrchestrator {
|
|
|
39
39
|
this.options = options;
|
|
40
40
|
this.config = null;
|
|
41
41
|
|
|
42
|
+
// Cloudflare API credentials for database operations
|
|
43
|
+
this.cloudflareToken = options.cloudflareToken;
|
|
44
|
+
this.cloudflareAccountId = options.cloudflareAccountId;
|
|
45
|
+
|
|
42
46
|
// Environment configurations
|
|
43
47
|
this.environments = {
|
|
44
48
|
development: {
|
|
@@ -389,11 +393,25 @@ export class DatabaseOrchestrator {
|
|
|
389
393
|
}
|
|
390
394
|
try {
|
|
391
395
|
// Validate database exists before attempting migrations
|
|
392
|
-
const exists = await databaseExists(databaseName
|
|
396
|
+
const exists = await databaseExists(databaseName, {
|
|
397
|
+
apiToken: this.cloudflareToken,
|
|
398
|
+
accountId: this.cloudflareAccountId
|
|
399
|
+
});
|
|
393
400
|
if (!exists) {
|
|
394
|
-
|
|
401
|
+
console.log(` 📦 Database ${databaseName} does not exist, creating...`);
|
|
402
|
+
if (!this.cloudflareToken || !this.cloudflareAccountId) {
|
|
403
|
+
throw new Error(`Database ${databaseName} does not exist and no Cloudflare API credentials provided. ` + `Cannot create database automatically. Please provide cloudflareToken and cloudflareAccountId.`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Create the database using API
|
|
407
|
+
await createDatabase(databaseName, {
|
|
408
|
+
apiToken: this.cloudflareToken,
|
|
409
|
+
accountId: this.cloudflareAccountId
|
|
410
|
+
});
|
|
411
|
+
console.log(` ✅ Database ${databaseName} created successfully`);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(` ✅ Database ${databaseName} validated`);
|
|
395
414
|
}
|
|
396
|
-
console.log(` ✅ Database ${databaseName} validated`);
|
|
397
415
|
|
|
398
416
|
// Use DATABASE name for wrangler command
|
|
399
417
|
const command = this.buildMigrationCommand(databaseName, environment, isRemote);
|
|
@@ -650,10 +668,13 @@ export class DatabaseOrchestrator {
|
|
|
650
668
|
|
|
651
669
|
buildMigrationCommand(databaseName, environment, isRemote) {
|
|
652
670
|
// Use DATABASE name, NOT binding name
|
|
653
|
-
// Wrangler expects: "npx wrangler d1 migrations apply database-name --
|
|
671
|
+
// Wrangler expects: "npx wrangler d1 migrations apply database-name --remote --env environment"
|
|
654
672
|
// NOT: "npx wrangler d1 migrations apply binding-name --local"
|
|
655
673
|
let command = `npx wrangler d1 migrations apply ${databaseName}`;
|
|
656
674
|
|
|
675
|
+
// Add environment flag for all environments (consistent with bin version)
|
|
676
|
+
command += ` --env ${environment}`;
|
|
677
|
+
|
|
657
678
|
// For remote environments, add --remote flag
|
|
658
679
|
// For local development, use --local
|
|
659
680
|
if (isRemote) {
|
|
@@ -35,6 +35,10 @@ export class MultiDomainOrchestrator {
|
|
|
35
35
|
this.parallelDeployments = options.parallelDeployments || 3;
|
|
36
36
|
this.servicePath = options.servicePath || process.cwd();
|
|
37
37
|
|
|
38
|
+
// Cloudflare credentials for API-based operations
|
|
39
|
+
this.cloudflareToken = options.cloudflareToken;
|
|
40
|
+
this.cloudflareAccountId = options.cloudflareAccountId;
|
|
41
|
+
|
|
38
42
|
// Initialize modular components
|
|
39
43
|
this.domainResolver = new DomainResolver({
|
|
40
44
|
environment: this.environment,
|
|
@@ -59,7 +63,9 @@ export class MultiDomainOrchestrator {
|
|
|
59
63
|
// Initialize enterprise-grade utilities
|
|
60
64
|
this.databaseOrchestrator = new DatabaseOrchestrator({
|
|
61
65
|
projectRoot: this.servicePath,
|
|
62
|
-
dryRun: this.dryRun
|
|
66
|
+
dryRun: this.dryRun,
|
|
67
|
+
cloudflareToken: this.cloudflareToken,
|
|
68
|
+
cloudflareAccountId: this.cloudflareAccountId
|
|
63
69
|
});
|
|
64
70
|
this.secretManager = new EnhancedSecretManager({
|
|
65
71
|
projectRoot: this.servicePath,
|
|
@@ -270,19 +276,49 @@ export class MultiDomainOrchestrator {
|
|
|
270
276
|
|
|
271
277
|
// Check if database already exists
|
|
272
278
|
console.log(` � Checking if database exists: ${databaseName}`);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log(`
|
|
279
|
+
let exists,
|
|
280
|
+
databaseId,
|
|
281
|
+
created = false;
|
|
282
|
+
|
|
283
|
+
// Use API-based operations if credentials are available
|
|
284
|
+
if (this.cloudflareToken && this.cloudflareAccountId) {
|
|
285
|
+
console.log(` 🔑 Using API token authentication for account: ${this.cloudflareAccountId}`);
|
|
286
|
+
exists = await databaseExists(databaseName, {
|
|
287
|
+
apiToken: this.cloudflareToken,
|
|
288
|
+
accountId: this.cloudflareAccountId
|
|
289
|
+
});
|
|
290
|
+
if (exists) {
|
|
291
|
+
console.log(` ✅ Database already exists: ${databaseName}`);
|
|
292
|
+
databaseId = await getDatabaseId(databaseName, {
|
|
293
|
+
apiToken: this.cloudflareToken,
|
|
294
|
+
accountId: this.cloudflareAccountId
|
|
295
|
+
});
|
|
296
|
+
console.log(` 📊 Existing Database ID: ${databaseId}`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(` 📦 Creating database: ${databaseName}`);
|
|
299
|
+
databaseId = await createDatabase(databaseName, {
|
|
300
|
+
apiToken: this.cloudflareToken,
|
|
301
|
+
accountId: this.cloudflareAccountId
|
|
302
|
+
});
|
|
303
|
+
console.log(` ✅ Database created: ${databaseName}`);
|
|
304
|
+
console.log(` 📊 Database ID: ${databaseId}`);
|
|
305
|
+
created = true;
|
|
306
|
+
}
|
|
280
307
|
} else {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
308
|
+
// Fallback to CLI-based operations (OAuth)
|
|
309
|
+
console.log(` 🔐 Using OAuth authentication (wrangler CLI)`);
|
|
310
|
+
exists = await databaseExists(databaseName);
|
|
311
|
+
if (exists) {
|
|
312
|
+
console.log(` ✅ Database already exists: ${databaseName}`);
|
|
313
|
+
databaseId = await getDatabaseId(databaseName);
|
|
314
|
+
console.log(` 📊 Existing Database ID: ${databaseId}`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(` 📦 Creating database: ${databaseName}`);
|
|
317
|
+
databaseId = await createDatabase(databaseName);
|
|
318
|
+
console.log(` ✅ Database created: ${databaseName}`);
|
|
319
|
+
console.log(` 📊 Database ID: ${databaseId}`);
|
|
320
|
+
created = true;
|
|
321
|
+
}
|
|
286
322
|
}
|
|
287
323
|
|
|
288
324
|
// Store database info in domain state
|
|
@@ -230,7 +230,22 @@ export async function listSecrets(env = 'production') {
|
|
|
230
230
|
throw new Error(`Failed to list secrets: ${error.message}`);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
export async function listDatabases() {
|
|
233
|
+
export async function listDatabases(options = {}) {
|
|
234
|
+
const {
|
|
235
|
+
apiToken,
|
|
236
|
+
accountId
|
|
237
|
+
} = options;
|
|
238
|
+
|
|
239
|
+
// Use API-based operation if credentials provided
|
|
240
|
+
if (apiToken && accountId) {
|
|
241
|
+
const {
|
|
242
|
+
CloudflareAPI
|
|
243
|
+
} = await import('../../../src/utils/cloudflare/api.js');
|
|
244
|
+
const cf = new CloudflareAPI(apiToken);
|
|
245
|
+
return await cf.listD1Databases(accountId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback to CLI-based operation
|
|
234
249
|
try {
|
|
235
250
|
const {
|
|
236
251
|
stdout: list
|
|
@@ -240,7 +255,22 @@ export async function listDatabases() {
|
|
|
240
255
|
throw new Error(`Failed to list databases: ${error.message}`);
|
|
241
256
|
}
|
|
242
257
|
}
|
|
243
|
-
export async function databaseExists(databaseName) {
|
|
258
|
+
export async function databaseExists(databaseName, options = {}) {
|
|
259
|
+
const {
|
|
260
|
+
apiToken,
|
|
261
|
+
accountId
|
|
262
|
+
} = options;
|
|
263
|
+
|
|
264
|
+
// Use API-based operation if credentials provided
|
|
265
|
+
if (apiToken && accountId) {
|
|
266
|
+
const {
|
|
267
|
+
CloudflareAPI
|
|
268
|
+
} = await import('../../../src/utils/cloudflare/api.js');
|
|
269
|
+
const cf = new CloudflareAPI(apiToken);
|
|
270
|
+
return await cf.d1DatabaseExists(accountId, databaseName);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Fallback to CLI-based operation
|
|
244
274
|
try {
|
|
245
275
|
const list = await listDatabases();
|
|
246
276
|
return list.includes(databaseName);
|
|
@@ -248,7 +278,23 @@ export async function databaseExists(databaseName) {
|
|
|
248
278
|
return false; // Assume doesn't exist if can't check
|
|
249
279
|
}
|
|
250
280
|
}
|
|
251
|
-
export async function createDatabase(name) {
|
|
281
|
+
export async function createDatabase(name, options = {}) {
|
|
282
|
+
const {
|
|
283
|
+
apiToken,
|
|
284
|
+
accountId
|
|
285
|
+
} = options;
|
|
286
|
+
|
|
287
|
+
// Use API-based operation if credentials provided
|
|
288
|
+
if (apiToken && accountId) {
|
|
289
|
+
const {
|
|
290
|
+
CloudflareAPI
|
|
291
|
+
} = await import('../../../src/utils/cloudflare/api.js');
|
|
292
|
+
const cf = new CloudflareAPI(apiToken);
|
|
293
|
+
const result = await cf.createD1Database(accountId, name);
|
|
294
|
+
return result.uuid; // Return UUID to match CLI behavior
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Fallback to CLI-based operation
|
|
252
298
|
try {
|
|
253
299
|
const {
|
|
254
300
|
stdout: output
|
|
@@ -347,7 +393,23 @@ export async function executeSql(databaseName, sql, env = 'production') {
|
|
|
347
393
|
}
|
|
348
394
|
|
|
349
395
|
// Get database ID from list output
|
|
350
|
-
export async function getDatabaseId(databaseName) {
|
|
396
|
+
export async function getDatabaseId(databaseName, options = {}) {
|
|
397
|
+
const {
|
|
398
|
+
apiToken,
|
|
399
|
+
accountId
|
|
400
|
+
} = options;
|
|
401
|
+
|
|
402
|
+
// Use API-based operation if credentials provided
|
|
403
|
+
if (apiToken && accountId) {
|
|
404
|
+
const {
|
|
405
|
+
CloudflareAPI
|
|
406
|
+
} = await import('../../../src/utils/cloudflare/api.js');
|
|
407
|
+
const cf = new CloudflareAPI(apiToken);
|
|
408
|
+
const db = await cf.getD1Database(accountId, databaseName);
|
|
409
|
+
return db?.uuid || null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Fallback to CLI-based operation
|
|
351
413
|
try {
|
|
352
414
|
const list = await listDatabases();
|
|
353
415
|
const lines = list.split('\n');
|
|
@@ -227,6 +227,57 @@ export class CloudflareAPI {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Create a D1 database
|
|
232
|
+
* @param {string} accountId - Account ID
|
|
233
|
+
* @param {string} name - Database name
|
|
234
|
+
* @returns {Promise<Object>} Created database info
|
|
235
|
+
*/
|
|
236
|
+
async createD1Database(accountId, name) {
|
|
237
|
+
const data = await this.request(`/accounts/${accountId}/d1/database`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
name
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
uuid: data.result.uuid,
|
|
245
|
+
name: data.result.name,
|
|
246
|
+
version: data.result.version,
|
|
247
|
+
createdAt: data.result.created_at
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if a D1 database exists
|
|
253
|
+
* @param {string} accountId - Account ID
|
|
254
|
+
* @param {string} name - Database name
|
|
255
|
+
* @returns {Promise<boolean>} True if database exists
|
|
256
|
+
*/
|
|
257
|
+
async d1DatabaseExists(accountId, name) {
|
|
258
|
+
try {
|
|
259
|
+
const databases = await this.listD1Databases(accountId);
|
|
260
|
+
return databases.some(db => db.name === name);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get D1 database by name
|
|
268
|
+
* @param {string} accountId - Account ID
|
|
269
|
+
* @param {string} name - Database name
|
|
270
|
+
* @returns {Promise<Object|null>} Database info or null if not found
|
|
271
|
+
*/
|
|
272
|
+
async getD1Database(accountId, name) {
|
|
273
|
+
try {
|
|
274
|
+
const databases = await this.listD1Databases(accountId);
|
|
275
|
+
return databases.find(db => db.name === name) || null;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
230
281
|
/**
|
|
231
282
|
* Helper: Get complete deployment info for a zone
|
|
232
283
|
* This combines zone details with useful metadata for deployment
|