/**
* Project Operations Module
*
* This module handles all project-level operations for Google Apps Script API:
* - List projects
* - Get project details
* - Get project content (files)
* - Get project metadata only
* - Create new projects
*
* Extracted from gasClient.ts for better modularity and maintainability.
*/
import { GASAuthOperations } from './gasAuthOperations.js';
import { GASProject, GASFile } from './gasTypes.js';
import { sortFilesForExecution } from './pathParser.js';
/**
* Project Operations class
* Manages Google Apps Script project-level operations
*/
export class GASProjectOperations {
private authOps: GASAuthOperations;
constructor(authOps: GASAuthOperations) {
this.authOps = authOps;
}
/**
* List all accessible projects
*/
async listProjects(pageSize: number = 10, accessToken?: string): Promise<GASProject[]> {
return this.authOps.makeApiCall(async () => {
console.error(`π Listing Apps Script projects via Drive API...`);
const driveApi = this.authOps.getDriveApi();
// Apps Script projects are Drive files with MIME type 'application/vnd.google-apps.script'
const response = await driveApi.files.list({
q: "mimeType='application/vnd.google-apps.script' and trashed=false",
pageSize,
fields: 'files(id,name,createdTime,modifiedTime,parents)'
});
const files = response.data.files || [];
console.error(`π Found ${files.length} Apps Script projects`);
return files.map((file: any) => ({
scriptId: file.id,
title: file.name,
parentId: file.parents?.[0],
createTime: file.createdTime,
updateTime: file.modifiedTime
}));
}, accessToken);
}
/**
* Get project details
*/
async getProject(scriptId: string, accessToken?: string): Promise<GASProject> {
await this.authOps.initializeClient(accessToken);
return this.authOps.makeApiCall(async () => {
const scriptApi = this.authOps.getScriptApi();
const response = await scriptApi.projects.get({
scriptId
});
return {
scriptId: response.data.scriptId,
title: response.data.title,
parentId: response.data.parentId,
createTime: response.data.createTime,
updateTime: response.data.updateTime
};
}, accessToken);
}
/**
* Get project content (files)
*/
async getProjectContent(scriptId: string, accessToken?: string): Promise<GASFile[]> {
return this.authOps.makeApiCall(async () => {
const scriptApi = this.authOps.getScriptApi();
const response = await scriptApi.projects.getContent({
scriptId
});
const files: GASFile[] = (response.data.files || []).map((file: any) => ({
name: file.name,
type: file.type,
source: file.source,
// β
NEW: Extract timestamp fields that API provides
createTime: file.createTime,
updateTime: file.updateTime,
lastModifyUser: file.lastModifyUser ? {
name: file.lastModifyUser.name,
email: file.lastModifyUser.email
} : undefined,
functionSet: file.functionSet
}));
// Sort files by execution order (currently preserves API order)
const sortedFiles = sortFilesForExecution(files);
// Capture position AFTER sorting to ensure position reflects actual execution order
return sortedFiles.map((file, index) => ({
...file,
position: index
}));
}, accessToken);
}
/**
* Get project metadata only (no source code content)
* ~100x faster than getProjectContent for sync verification
*/
async getProjectMetadata(scriptId: string, accessToken?: string): Promise<GASFile[]> {
return this.authOps.makeApiCall(async () => {
const scriptApi = this.authOps.getScriptApi();
const response = await scriptApi.projects.getContent({
scriptId,
// Exclude 'source' field for efficiency - only get metadata
fields: 'files(name,type,createTime,updateTime,lastModifyUser)'
});
const files: GASFile[] = (response.data.files || []).map((file: any) => ({
name: file.name,
type: file.type,
// No source field - metadata only
createTime: file.createTime,
updateTime: file.updateTime,
lastModifyUser: file.lastModifyUser ? {
name: file.lastModifyUser.name,
email: file.lastModifyUser.email
} : undefined
}));
return sortFilesForExecution(files);
}, accessToken);
}
/**
* Create new project
*/
async createProject(title: string, parentId?: string, accessToken?: string): Promise<GASProject> {
await this.authOps.initializeClient(accessToken);
return this.authOps.makeApiCall(async () => {
const scriptApi = this.authOps.getScriptApi();
const requestBody: any = {
title
};
// Only include parentId if it's provided (avoid sending undefined)
if (parentId) {
requestBody.parentId = parentId;
}
console.error(`\nπ [PROJECT CREATE] ULTRA DEBUG - Request Details:`);
console.error(` π Parameters:`);
console.error(` - title: "${title}" (length: ${title.length})`);
console.error(` - parentId: ${parentId || 'undefined'}`);
console.error(` - accessToken: ${accessToken ? 'present (' + accessToken.substring(0, 20) + '...)' : 'undefined'}`);
console.error(` π¦ Request Body:`);
console.error(` - Raw object:`, requestBody);
console.error(` - JSON serialized:`, JSON.stringify(requestBody));
console.error(` - JSON pretty:`, JSON.stringify(requestBody, null, 2));
console.error(` - Body byte length: ${JSON.stringify(requestBody).length}`);
console.error(` π API Details:`);
console.error(` - Method: POST`);
console.error(` - URL: https://script.googleapis.com/v1/projects`);
console.error(` - Expected Content-Type: application/json`);
console.error(` π Auth Context:`);
console.error(` - Client initialized: ${scriptApi ? 'YES' : 'NO'}`);
console.error(` - Using session auth: ${!accessToken}`);
// Capture the raw request before it's sent
const requestOptions = {
requestBody
};
console.error(` π Final googleapis request options:`, JSON.stringify(requestOptions, null, 2));
const startTime = Date.now();
try {
console.error(`\nπ [PROJECT CREATE] MAXIMUM DETAIL - Sending API request...`);
console.error(` π EXACT URL: https://script.googleapis.com/v1/projects`);
console.error(` π METHOD: POST`);
console.error(` π¦ EXACT PAYLOAD: ${JSON.stringify(requestBody)}`);
console.error(` π PAYLOAD SIZE: ${JSON.stringify(requestBody).length} bytes`);
console.error(` π AUTH HEADER: Bearer ${accessToken ? accessToken.substring(0, 30) + '...[REDACTED]' : '[SESSION_TOKEN]'}`);
console.error(` β° REQUEST TIMESTAMP: ${new Date().toISOString()}`);
console.error(` π GOOGLEAPIS OPTIONS:`, JSON.stringify(requestOptions, null, 2));
const response = await scriptApi.projects.create(requestOptions);
const duration = Date.now() - startTime;
console.error(`\nβ
[PROJECT CREATE] SUCCESS Response Details:`);
console.error(` β° RESPONSE TIME: ${duration}ms`);
console.error(` π HTTP STATUS: ${response.status}`);
console.error(` π STATUS TEXT: ${response.statusText}`);
console.error(` π RESPONSE URL: ${response.config?.url || 'Unknown'}`);
console.error(` π¦ RESPONSE HEADERS:`);
Object.entries(response.headers || {}).forEach(([key, value]) => {
console.error(` ${key}: ${value}`);
});
console.error(` π RESPONSE BODY:`, JSON.stringify(response.data, null, 2));
console.error(` π RESPONSE SIZE: ${JSON.stringify(response.data).length} bytes`);
console.error(` π FULL RESPONSE CONFIG:`, JSON.stringify(response.config, null, 2));
return {
scriptId: response.data.scriptId,
title: response.data.title,
parentId: response.data.parentId,
createTime: response.data.createTime,
updateTime: response.data.updateTime
};
} catch (apiError: any) {
const errorDuration = Date.now() - startTime;
console.error(`\nβ [PROJECT CREATE] MAXIMUM ERROR DETAIL Analysis:`);
console.error(` β° ERROR AFTER: ${errorDuration}ms`);
console.error(` π ERROR TYPE: ${apiError.constructor?.name}`);
console.error(` π ERROR MESSAGE: ${apiError.message}`);
console.error(` π HTTP STATUS: ${apiError.response?.status || apiError.status || 'Unknown'}`);
console.error(` π STATUS TEXT: ${apiError.response?.statusText || 'Unknown'}`);
console.error(` π FAILED URL: ${apiError.config?.url || 'https://script.googleapis.com/v1/projects'}`);
console.error(` π FAILED METHOD: ${apiError.config?.method || 'POST'}`);
console.error(`\nπ€ ORIGINAL REQUEST DETAILS:`);
console.error(` π URL: https://script.googleapis.com/v1/projects`);
console.error(` π METHOD: POST`);
console.error(` π¦ SENT PAYLOAD: ${JSON.stringify(requestBody)}`);
console.error(` π SENT PAYLOAD SIZE: ${JSON.stringify(requestBody).length} bytes`);
if (apiError.response) {
console.error(`\nπ₯ ERROR RESPONSE DETAILS:`);
console.error(` π RESPONSE STATUS: ${apiError.response.status}`);
console.error(` π RESPONSE STATUS TEXT: ${apiError.response.statusText}`);
console.error(` π¦ RESPONSE HEADERS:`);
Object.entries(apiError.response.headers || {}).forEach(([key, value]) => {
console.error(` ${key}: ${value}`);
});
console.error(` π RESPONSE BODY:`, JSON.stringify(apiError.response.data, null, 2));
console.error(` π RESPONSE SIZE: ${JSON.stringify(apiError.response.data || {}).length} bytes`);
if (apiError.response.config) {
console.error(`\nπ§ REQUEST CONFIG FROM ERROR:`);
console.error(` π CONFIG URL: ${apiError.response.config.url}`);
console.error(` π CONFIG METHOD: ${apiError.response.config.method}`);
console.error(` π¦ CONFIG HEADERS:`, JSON.stringify(apiError.response.config.headers, null, 2));
console.error(` π CONFIG DATA/BODY: ${apiError.response.config.body || apiError.response.config.data || 'None'}`);
console.error(` π§ CONFIG PARAMS:`, JSON.stringify(apiError.response.config.params, null, 2));
}
}
if (apiError.config && !apiError.response) {
console.error(`\nπ§ ERROR CONFIG (No Response):`);
console.error(` π CONFIG URL: ${apiError.config.url}`);
console.error(` π CONFIG METHOD: ${apiError.config.method}`);
console.error(` π¦ CONFIG HEADERS:`, JSON.stringify(apiError.config.headers, null, 2));
console.error(` π CONFIG DATA: ${apiError.config.data || 'None'}`);
}
console.error(`\nπ COMPLETE ERROR OBJECT:`, JSON.stringify(apiError, null, 2));
console.error(`\nπ ERROR STACK TRACE:`);
console.error(apiError.stack);
// Re-throw the error to be handled by makeApiCall
throw apiError;
}
}, accessToken);
}
}