Syntax Project


https://nodejs.org/api/esm.html
https://nodejs.org/api/packages.html
https://nodejs.org/api/crypto.html
https://www.prisma.io/docs/orm
https://www.prisma.io/docs/orm/prisma-client/queries/crud

1. Goal#

目标:
    用一个尽可能小的 order project 覆盖 Node.js / JavaScript 常用语法
    使用 Prisma 作为 Node.js 常见 ORM 示例
    每段代码都用注释标明用了什么语法,以及为什么这么用

you will see:
    ESM import/export
    object / array / destructuring / spread
    class
    async / await
    Map
    nullish coalescing
    optional field handling
    switch
    error handling
    Prisma schema
    Prisma Client

2. Project Structure#

node-syntax-project
├── package.json
├── .env
├── prisma
│   └── schema.prisma
└── src
    ├── main.js
    ├── prisma.js
    ├── repository.js
    └── service.js

3. Package / Build Files#

package.json#

{
  "name": "node-syntax-project",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "node src/main.js",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev --name init"
  },
  "dependencies": {
    "@prisma/client": "latest"
  },
  "devDependencies": {
    "prisma": "latest"
  }
}

.env#

DATABASE_URL="file:./dev.db"

prisma/schema.prisma#

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Order {
  orderId   String      @id @default(uuid())
  userId    String
  status    String
  createdAt DateTime    @default(now())
  paidAt    DateTime?
  items     OrderItem[]
}

model OrderItem {
  id       Int    @id @default(autoincrement())
  orderId  String
  sku      String
  name     String
  quantity Int
  amount   Float
  currency String
  order    Order  @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
}

4. Main Source Files#

src/prisma.js#

import { PrismaClient } from "@prisma/client";

// singleton: 一个进程复用一个 PrismaClient,避免频繁创建数据库连接
export const prisma = new PrismaClient();

src/repository.js#

export class PrismaOrderRepository {
  // class + constructor: 把 PrismaClient 作为依赖注入,方便测试替换
  constructor(db) {
    this.db = db;
  }

  async findById(orderId) {
    const record = await this.db.order.findUnique({
      where: { orderId },
      // ORM relation: include items,一次查出 order 和 order_items
      include: { items: true }
    });

    // null check: Prisma 查不到时返回 null,service 层可以明确处理
    if (record === null) {
      return null;
    }

    // object literal + map: 把 ORM record 映射成业务对象
    return {
      orderId: record.orderId,
      userId: record.userId,
      status: record.status,
      createdAt: record.createdAt,
      // spread + conditional: paidAt 为空时不放进对象
      ...(record.paidAt === null ? {} : { paidAt: record.paidAt }),
      items: record.items.map((item) => ({
        sku: item.sku,
        name: item.name,
        quantity: item.quantity,
        price: {
          amount: item.amount,
          currency: item.currency
        }
      }))
    };
  }

  async save(order) {
    await this.db.order.upsert({
      where: { orderId: order.orderId },
      update: {
        status: order.status,
        paidAt: order.paidAt,
        items: {
          deleteMany: {},
          create: order.items.map((item) => ({
            sku: item.sku,
            name: item.name,
            quantity: item.quantity,
            amount: item.price.amount,
            currency: item.price.currency
          }))
        }
      },
      create: {
        orderId: order.orderId,
        userId: order.userId,
        status: order.status,
        createdAt: order.createdAt,
        paidAt: order.paidAt,
        items: {
          create: order.items.map((item) => ({
            sku: item.sku,
            name: item.name,
            quantity: item.quantity,
            amount: item.price.amount,
            currency: item.price.currency
          }))
        }
      }
    });

    return order;
  }

  async list() {
    const records = await this.db.order.findMany({
      include: { items: true },
      orderBy: { createdAt: "desc" }
    });

    // Promise.all: 并行等待多个 async mapping 结果
    return Promise.all(records.map((record) => this.findById(record.orderId)))
      // filter(Boolean): 去掉 null 结果,保持返回 order list
      .then((orders) => orders.filter(Boolean));
  }
}

src/service.js#

import { randomUUID } from "node:crypto";

export class OrderService {
  constructor(repository) {
    this.repository = repository;
  }

  async create(input) {
    const order = {
      orderId: randomUUID(),
      userId: input.userId,
      status: "PENDING",
      items: input.items,
      createdAt: new Date()
    };

    await this.repository.save(order);
    this.handleAudit({ type: "ORDER_CREATED", orderId: order.orderId, at: new Date() });

    return order;
  }

  async pay(orderId) {
    const order = await this.repository.findById(orderId);

    if (order === null) {
      return { ok: false, error: "ORDER_NOT_FOUND" };
    }

    if (order.status !== "PENDING") {
      return { ok: false, error: "INVALID_STATUS" };
    }

    // spread: 基于旧对象创建新状态,不直接修改原对象
    const paidOrder = {
      ...order,
      status: "PAID",
      paidAt: new Date()
    };

    await this.repository.save(paidOrder);
    this.handleAudit({ type: "ORDER_PAID", orderId, transactionId: randomUUID(), at: new Date() });

    return { ok: true, value: paidOrder };
  }

  async summarize([limit = 20] = []) {
    const orders = await this.repository.list();

    return orders.slice(0, limit).map((order) => ({
      orderId: order.orderId,
      userId: order.userId,
      status: order.status,
      // reduce: 把多个 item 金额聚合成订单总额
      totalAmount: order.items.reduce(
        (sum, item) => sum + item.quantity * item.price.amount,
        0
      )
    }));
  }

  handleAudit(event) {
    // switch: 根据 event.type 明确处理不同事件结构
    switch (event.type) {
      case "ORDER_CREATED":
        console.log(`created ${event.orderId}`);
        return;
      case "ORDER_PAID":
        console.log(`paid ${event.orderId} by ${event.transactionId}`);
        return;
      default:
        throw new Error(`unknown event type: ${event.type}`);
    }
  }
}

src/main.js#

import { prisma } from "./prisma.js";
import { PrismaOrderRepository } from "./repository.js";
import { OrderService } from "./service.js";

const repository = new PrismaOrderRepository(prisma);
const service = new OrderService(repository);

const order = await service.create({
  userId: "u-1001",
  items: [
    {
      sku: "sku-1001",
      name: "Keyboard",
      quantity: 1,
      price: {
        amount: 99.9,
        currency: "USD"
      }
    }
  ]
});

const paid = await service.pay(order.orderId);

// discriminated result pattern: ok=true 时读 value,ok=false 时读 error
if (paid.ok) {
  console.log("paid order", paid.value);
} else {
  console.log("pay failed", paid.error);
}

console.log(await service.summarize([10]));

await prisma.$disconnect();

5. Run#

npm install
npm run prisma:migrate
npm run dev

6. Reading Order#

read files in this order:
    1. prisma/schema.prisma
    2. prisma.js
    3. repository.js
    4. service.js
    5. main.js

理解重点:
    Prisma schema defines database model
    repository maps ORM records to business objects
    service owns business logic
    main wires everything together