Node.js অ্যাডমিন SDK ব্যবহার করে Firestore এর সাথে ক্লাউড রানে একটি সম্পূর্ণ স্ট্যাক অ্যাঙ্গুলার অ্যাপ্লিকেশন স্থাপন করুন

১. সংক্ষিপ্ত বিবরণ

ক্লাউড রান একটি সম্পূর্ণভাবে পরিচালিত প্ল্যাটফর্ম যা আপনাকে গুগলের পরিবর্ধনযোগ্য পরিকাঠামোর উপর সরাসরি আপনার কোড চালানোর সুযোগ দেয়। এই কোডল্যাবে দেখানো হবে কিভাবে Node.js অ্যাডমিন SDK ব্যবহার করে ক্লাউড রানে থাকা একটি অ্যাঙ্গুলার অ্যাপ্লিকেশনকে ফায়ারস্টোর ডেটাবেসের সাথে সংযুক্ত করা যায়।

এই ল্যাবে, আপনি শিখবেন কীভাবে:

  • একটি ফায়ারস্টোর ডাটাবেস তৈরি করুন
  • ক্লাউড রান-এ এমন একটি অ্যাপ্লিকেশন স্থাপন করুন যা আপনার ফায়ারস্টোর ডেটাবেসের সাথে সংযুক্ত হয়।

২. পূর্বশর্তসমূহ

  1. যদি আপনার আগে থেকে কোনো গুগল অ্যাকাউন্ট না থাকে, তাহলে আপনাকে অবশ্যই একটি গুগল অ্যাকাউন্ট তৈরি করতে হবে।
    • কর্মক্ষেত্র বা শিক্ষা প্রতিষ্ঠানের অ্যাকাউন্টের পরিবর্তে ব্যক্তিগত অ্যাকাউন্ট ব্যবহার করুন। কর্মক্ষেত্র এবং শিক্ষা প্রতিষ্ঠানে এমন কিছু সীমাবদ্ধতা থাকতে পারে, যার ফলে আপনি এই ল্যাবের জন্য প্রয়োজনীয় এপিআই (API) সক্রিয় করতে পারবেন না।

৩. প্রজেক্ট সেটআপ

  1. গুগল ক্লাউড কনসোলে সাইন-ইন করুন।
  2. ক্লাউড কনসোলে বিলিং চালু করুন
    • এই ল্যাবটি সম্পন্ন করতে ক্লাউড রিসোর্সে ১ মার্কিন ডলারেরও কম খরচ হওয়া উচিত।
    • পরবর্তী চার্জ এড়াতে, এই ল্যাবের শেষে দেওয়া ধাপগুলো অনুসরণ করে আপনি রিসোর্সগুলো মুছে ফেলতে পারেন।
    • নতুন ব্যবহারকারীরা ৩০০ মার্কিন ডলারের ফ্রি ট্রায়ালের জন্য যোগ্য।
  3. একটি নতুন প্রজেক্ট তৈরি করুন অথবা বিদ্যমান কোনো প্রজেক্ট পুনরায় ব্যবহার করুন।

৪. ক্লাউড শেল এডিটর খুলুন

  1. ক্লাউড শেল এডিটরে যান
  2. যদি স্ক্রিনের নীচে টার্মিনালটি দেখা না যায়, তাহলে এটি খুলুন:
    • হ্যামবার্গার মেনুতে ক্লিক করুন হ্যামবার্গার মেনু আইকন
    • টার্মিনালে ক্লিক করুন
    • নতুন টার্মিনালে ক্লিক করুন ক্লাউড শেল এডিটরে নতুন টার্মিনাল খুলুন
  3. টার্মিনালে এই কমান্ডটি দিয়ে আপনার প্রজেক্ট সেট করুন:
    • বিন্যাস:
      gcloud config set project [PROJECT_ID]
      
    • উদাহরণ:
      gcloud config set project lab-project-id-example
      
    • যদি আপনি আপনার প্রজেক্ট আইডি মনে করতে না পারেন:
      • আপনি আপনার সমস্ত প্রজেক্ট আইডি তালিকাভুক্ত করতে পারেন:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      ক্লাউড শেল এডিটর টার্মিনালে প্রজেক্ট আইডি সেট করুন
  4. অনুমোদন করতে বলা হলে, চালিয়ে যাওয়ার জন্য 'Authorize'-এ ক্লিক করুন। ক্লাউড শেল অনুমোদন করতে ক্লিক করুন
  5. আপনি এই বার্তাটি দেখতে পাবেন:
    Updated property [core/project].
    
    যদি আপনি একটি WARNING দেখতে পান এবং আপনাকে Do you want to continue (Y/N)? জিজ্ঞাসা করা হয়, তাহলে সম্ভবত আপনি প্রজেক্ট আইডি ভুলভাবে প্রবেশ করিয়েছেন। N চাপুন, Enter চাপুন এবং gcloud config set project কমান্ডটি আবার চালানোর চেষ্টা করুন।

৫. এপিআই সক্রিয় করুন

টার্মিনালে, এপিআইগুলো সক্রিয় করুন:

gcloud services enable \
  firestore.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

অনুমোদন করতে বলা হলে, চালিয়ে যাওয়ার জন্য 'Authorize'-এ ক্লিক করুন। ক্লাউড শেল অনুমোদন করতে ক্লিক করুন

এই কমান্ডটি সম্পন্ন হতে কয়েক মিনিট সময় লাগতে পারে, কিন্তু অবশেষে এটি এইটির মতো একটি সফলতার বার্তা দেবে:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

৬. ফায়ারস্টোর ডেটাবেস তৈরি করুন

  1. একটি ফায়ারস্টোর ডাটাবেস তৈরি করতে gcloud firestore databases create কমান্ডটি চালান।
    gcloud firestore databases create --location=nam5
    

৭. আবেদনপত্র প্রস্তুত করুন

একটি Next.js অ্যাপ্লিকেশন তৈরি করুন যা HTTP অনুরোধে সাড়া দেয়।

  1. task-app নামের একটি নতুন Next.js প্রজেক্ট তৈরি করতে, এই কমান্ডটি ব্যবহার করুন:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
    
  2. task-app ডিরেক্টরি পরিবর্তন করুন:
    cd task-app
    
  1. ফায়ারস্টোর ডাটাবেসের সাথে সংযোগ স্থাপনের জন্য firebase-admin ইনস্টল করুন।
    npm install firebase-admin
    
  1. ক্লাউড শেল এডিটরে server.ts ফাইলটি খুলুন:
    cloudshell edit src/server.ts
    
    এখন স্ক্রিনের উপরের অংশে একটি ফাইল দেখা যাবে। এখানেই আপনি server.ts ফাইলটি সম্পাদনা করতে পারবেন। দেখান যে কোডটি স্ক্রিনের উপরের অংশে থাকবে।
  2. server.ts ফাইলের বিদ্যমান বিষয়বস্তু মুছে ফেলুন।
  3. নিচের কোডটি কপি করে খোলা server.ts ফাইলে পেস্ট করুন:
    import {
      AngularNodeAppEngine,
      createNodeRequestHandler,
      isMainModule,
      writeResponseToNodeResponse,
    } from '@angular/ssr/node';
    import express from 'express';
    import { dirname, resolve } from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    const credential = applicationDefault();
    
    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }
    
    const db = getFirestore();
    const tasksRef = db.collection('tasks');
    
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');
    
    const app = express();
    const angularApp = new AngularNodeAppEngine();
    
    app.use(express.json());
    
    app.get('/api/tasks', async (req, res) => {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks: Task[] = snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data()['title'],
        status: doc.data()['status'],
        createdAt: doc.data()['createdAt'],
      }));
      res.send(tasks);
    });
    
    app.post('/api/tasks', async (req, res) => {
      const newTaskTitle = req.body.title;
      if(!newTaskTitle){
        res.status(400).send("Title is required");
        return;
      }
      await tasksRef.doc().create({
        title: newTaskTitle,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      res.sendStatus(200);
    });
    
    app.put('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if (!task || !task.id || !task.title || !task.status) {
        res.status(400).send("Invalid task data");
        return;
      }
      await tasksRef.doc(task.id).set(task);
      res.sendStatus(200);
    });
    
    app.delete('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if(!task || !task.id){
        res.status(400).send("Task ID is required");
        return;
      }
      await tasksRef.doc(task.id).delete();
      res.sendStatus(200);
    });
    
    /**
    * Serve static files from /browser
    */
    app.use(
      express.static(browserDistFolder, {
        maxAge: '1y',
        index: false,
        redirect: false,
      }),
    );
    
    /**
    * Handle all other requests by rendering the Angular application.
    */
    app.use('/**', (req, res, next) => {
      angularApp
        .handle(req)
        .then((response) =>
          response ? writeResponseToNodeResponse(response, res) : next(),
        )
        .catch(next);
    });
    
    /**
    * Start the server if this module is the main entry point.
    * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
    */
    if (isMainModule(import.meta.url)) {
      const port = process.env['PORT'] || 4000;
      app.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }
    
    /**
    * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
    */
    export const reqHandler = createNodeRequestHandler(app);
    
  4. ক্লাউড শেল এডিটর-এ angular.json ফাইলটি খুলুন:
    cloudshell edit angular.json
    
    আমরা এখন angular.json ফাইলে "externalDependencies": ["firebase-admin"] লাইনটি যোগ করব।
  5. angular.json ফাইলের বিদ্যমান বিষয়বস্তু মুছে ফেলুন।
  6. নিচের কোডটি কপি করে খোলা angular.json ফাইলে পেস্ট করুন:
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "task-app": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:component": {
              "inlineTemplate": true,
              "inlineStyle": true,
              "skipTests": true
            },
            "@schematics/angular:class": {
              "skipTests": true
            },
            "@schematics/angular:directive": {
              "skipTests": true
            },
            "@schematics/angular:guard": {
              "skipTests": true
            },
            "@schematics/angular:interceptor": {
              "skipTests": true
            },
            "@schematics/angular:pipe": {
              "skipTests": true
            },
            "@schematics/angular:resolver": {
              "skipTests": true
            },
            "@schematics/angular:service": {
              "skipTests": true
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:application",
              "options": {
                "outputPath": "dist/task-app",
                "index": "src/index.html",
                "browser": "src/main.ts",
                "polyfills": [
                  "zone.js"
                ],
                "tsConfig": "tsconfig.app.json",
                "assets": [
                  {
                    "glob": "**/*",
                    "input": "public"
                  }
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [],
                "server": "src/main.server.ts",
                "outputMode": "server",
                "ssr": {
                  "entry": "src/server.ts"
                },
                "externalDependencies": ["firebase-admin"]
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kB",
                      "maximumError": "1MB"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "4kB",
                      "maximumError": "8kB"
                    }
                  ],
                  "outputHashing": "all"
                },
                "development": {
                  "optimization": false,
                  "extractLicenses": false,
                  "sourceMap": true
                }
              },
              "defaultConfiguration": "production"
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "configurations": {
                "production": {
                  "buildTarget": "task-app:build:production"
                },
                "development": {
                  "buildTarget": "task-app:build:development"
                }
              },
              "defaultConfiguration": "development"
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n"
            }
          }
        }
      }
    }
    

"externalDependencies": ["firebase-admin"]

  1. ক্লাউড শেল এডিটরে app.component.ts ফাইলটি খুলুন:
    cloudshell edit src/app/app.component.ts
    
    এখন স্ক্রিনের উপরের অংশে একটি বিদ্যমান ফাইল দেখা যাবে। এখানেই আপনি app.component.ts ফাইলটি সম্পাদনা করতে পারবেন। দেখান যে কোডটি স্ক্রিনের উপরের অংশে থাকবে।
  2. app.component.ts ফাইলের বিদ্যমান বিষয়বস্তু মুছে ফেলুন।
  3. নিচের কোডটি কপি করে খোলা app.component.ts ফাইলে পেস্ট করুন:
    import { afterNextRender, Component, signal } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [FormsModule],
      template: `
        <section>
          <input
            type="text"
            placeholder="New Task Title"
            [(ngModel)]="newTaskTitle"
            class="text-black border-2 p-2 m-2 rounded"
          />
          <button (click)="addTask()">Add new task</button>
          <table>
            <tbody>
              @for (task of tasks(); track task) {
                @let isComplete = task.status === 'COMPLETE';
                <tr>
                  <td>
                    <input
                      (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })"
                      type="checkbox"
                      [checked]="isComplete"
                    />
                  </td>
                  <td>{{ task.title }}</td>
                  <td>{{ task.status }}</td>
                  <td>
                    <button (click)="deleteTask(task)">Delete</button>
                  </td>
                </tr>
              }
            </tbody>
          </table>
        </section>
      `,
      styles: '',
    })
    export class AppComponent {
      newTaskTitle = '';
      tasks = signal<Task[]>([]);
    
      constructor() {
        afterNextRender({
          earlyRead: () => this.getTasks()
        });
      }
    
      async getTasks() {
        const response = await fetch(`/api/tasks`);
        const tasks = await response.json();
        this.tasks.set(tasks);
      }
    
      async addTask() {
        await fetch(`/api/tasks`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            title: this.newTaskTitle,
            status: 'IN_PROGRESS',
            createdAt: Date.now(),
          }),
        });
        this.newTaskTitle = '';
        await this.getTasks();
      }
    
      async updateTask(task: Task, newTaskValues: Partial<Task>) {
        await fetch(`/api/tasks`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...task, ...newTaskValues }),
        });
        await this.getTasks();
      }
    
      async deleteTask(task: any) {
        await fetch('/api/tasks', {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task),
        });
        await this.getTasks();
      }
    }
    

অ্যাপ্লিকেশনটি এখন ডেপ্লয় করার জন্য প্রস্তুত।

৮. অ্যাপ্লিকেশনটি ক্লাউড রান-এ স্থাপন করুন।

  1. আপনার অ্যাপ্লিকেশনটি ক্লাউড রান-এ ডেপ্লয় করতে নিচের কমান্ডটি চালান:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
    
  2. অনুরোধ করা হলে, আপনি যে চালিয়ে যেতে চান তা নিশ্চিত করতে Y এবং Enter চাপুন:
    Do you want to continue (Y/n)? Y
    

কয়েক মিনিট পর অ্যাপ্লিকেশনটি আপনাকে ভিজিট করার জন্য একটি URL প্রদান করবে।

আপনার অ্যাপ্লিকেশনটি বাস্তবে দেখতে ইউআরএল-টিতে যান। প্রতিবার ইউআরএল-টিতে গেলে বা পৃষ্ঠাটি রিফ্রেশ করলে, আপনি টাস্ক অ্যাপটি দেখতে পাবেন।

৯. অভিনন্দন

এই ল্যাবে, আপনি নিম্নলিখিত বিষয়গুলো করতে শিখেছেন:

  • একটি ক্লাউড এসকিউএল ফর পোস্টগ্রেসকিউএল ইনস্ট্যান্স তৈরি করুন
  • ক্লাউড রান-এ এমন একটি অ্যাপ্লিকেশন স্থাপন করুন যা আপনার ক্লাউড SQL ডেটাবেসের সাথে সংযোগ স্থাপন করে।

পরিষ্কার করা

ক্লাউড এসকিউএল-এর কোনো ফ্রি টিয়ার নেই এবং এটি ব্যবহার করতে থাকলে আপনাকে চার্জ করা হবে। অতিরিক্ত চার্জ এড়ানোর জন্য আপনি আপনার ক্লাউড প্রজেক্টটি ডিলিট করে দিতে পারেন।

পরিষেবাটি ব্যবহার না করা হলে ক্লাউড রান কোনো চার্জ না করলেও, আর্টিফ্যাক্ট রেজিস্ট্রি-তে কন্টেইনার ইমেজ সংরক্ষণের জন্য আপনাকে চার্জ করা হতে পারে। আপনার ক্লাউড প্রজেক্টটি ডিলিট করে দিলে সেই প্রজেক্টের মধ্যে ব্যবহৃত সমস্ত রিসোর্সের বিলিং বন্ধ হয়ে যায়।

আপনি চাইলে প্রজেক্টটি মুছে ফেলতে পারেন:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

আপনি আপনার ক্লাউডশেল ডিস্ক থেকে অপ্রয়োজনীয় রিসোর্সগুলো মুছে ফেলতে চাইতে পারেন। আপনি যা করতে পারেন:

  1. কোডল্যাব প্রজেক্ট ডিরেক্টরিটি মুছে ফেলুন:
    rm -rf ~/task-app
    
  2. সতর্কীকরণ! এই পরবর্তী কাজটি পূর্বাবস্থায় ফেরানো যাবে না! জায়গা খালি করার জন্য আপনি যদি আপনার ক্লাউড শেলের সবকিছু মুছে ফেলতে চান, তাহলে আপনি আপনার সম্পূর্ণ হোম ডিরেক্টরিটি ডিলিট করে দিতে পারেন। খেয়াল রাখবেন, আপনি যা কিছু রাখতে চান তা যেন অন্য কোথাও সেভ করা থাকে।
    sudo rm -rf $HOME
    

শিখতে থাকুন