@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 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
- throw new Error(`Database ${databaseName} does not exist. ` + `Database must be created before applying migrations.`);
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 --local"
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
- const exists = await databaseExists(databaseName);
274
- let databaseId;
275
- let created = false;
276
- if (exists) {
277
- console.log(` ✅ Database already exists: ${databaseName}`);
278
- databaseId = await getDatabaseId(databaseName);
279
- console.log(` 📊 Existing Database ID: ${databaseId}`);
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
- console.log(` 📦 Creating database: ${databaseName}`);
282
- databaseId = await createDatabase(databaseName);
283
- console.log(` ✅ Database created: ${databaseName}`);
284
- console.log(` 📊 Database ID: ${databaseId}`);
285
- created = true;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [