การดูแลระบบ › การอัปเกรด
เอกสาร/การดูแลระบบ/การอัปเกรด

การอัปเกรด

สิ่งที่จะได้เรียนรู้

แต่ละเวอร์ชันเปลี่ยนอะไรไปบ้าง และต้องแก้อะไรในแอปเมื่ออัปเกรด

ก่อนหน้านี้: การตั้งค่า

อัปเกรดแพ็กเกจ @easy-cms/* และ easy-cms ทุกตัวพร้อมกัน เพราะออกเวอร์ชันด้วยเลขเดียวกัน

npm
npm install @easy-cms/core@latest easy-cms@latest
pnpm
pnpm add @easy-cms/core@latest easy-cms@latest
yarn
yarn add @easy-cms/core@latest easy-cms@latest
bun
bun add @easy-cms/core@latest easy-cms@latest

ใส่แพ็กเกจ @easy-cms/* อื่นที่แอปใช้ (@easy-cms/nuxt, @easy-cms/db-sqlite, …) ในคำสั่งเดียวกัน

ก่อน 1.0 เวอร์ชัน minor (0.48 → 0.60) อาจเปลี่ยนชื่อได้ แต่ละหัวข้อด้านล่างบอกว่าต้องแก้อะไร ถ้า config ยังใช้ชื่อเก่า แอปจะไม่เริ่มทำงาน และ error จะบอกชื่อใหม่:

Invalid Easy CMS config (1 problem):
  • admin.siteUrl: is now `siteURL`

จาก 0.x ไป 1.0

0.60 ล็อกชื่อที่ 1.0 จะใช้ต่อ ส่วน release หลังจากนั้นเพิ่มความปลอดภัยและเตรียม plugin ทดลองให้พร้อมใช้ แอปที่ยังอยู่ 0.48 หรือเก่ากว่าจะเจอทุกอย่างพร้อมกัน การเปลี่ยนแปลงที่ทำให้แอปไม่เริ่มทำงานจะบอกเองพร้อมชื่อใหม่ แต่รายการในเช็คลิสต์ไม่บอก จึงควรอ่านก่อน deploy

มีอะไรเปลี่ยน

  • ชื่อ ของ option ใน config, เมธอดของ Local API, คำสั่ง CLI และ option ของ plugin (0.60) option ของ config ที่เปลี่ยนชื่อจะทำให้แอปไม่เริ่มทำงานและบอกชื่อใหม่
  • Route ของ REST: การ sign in ย้ายไป <api>/auth/* ส่วน path เดิม /users/*, POST ไปที่ global และ ?fallback-locale= ยังใช้ได้ตลอด 1.x พร้อมคำเตือนการเลิกใช้
  • Error มี code (NOT_FOUND, VALIDATION_ERROR…) ให้เช็คค่านี้แทนข้อความ
  • ค่าเริ่มต้นด้านความปลอดภัย ทีละ release ตั้งแต่ 0.62 ถึง 0.66
  • GraphQL ต้องติดตั้ง graphql ในโปรเจกต์ (0.68) ถ้าไม่มีแอปจะไม่เริ่มทำงาน (Cannot find module 'graphql') npm, pnpm และ Bun ติดตั้งให้เอง แต่ Yarn ไม่ติดตั้งให้

เช็คลิสต์: การเปลี่ยนแปลงที่ไม่ทำให้แอปหยุด

ถ้าแอปของคุณ…ให้ทำ
พึ่ง webhook ของ usersใส่ users ใน collections ของ webhook (0.60)
ใช้ isAdmin กับ admin ของ tenant เดียวเขียนกฎเอง ตอนนี้ isAdmin หมายถึงทั้งเว็บ (0.60)
คาดว่าจะได้ 403 เมื่อแก้เอกสารที่ผู้ใช้อ่านไม่ได้ตอนนี้ได้ 404 (0.63)
อยู่หลัง reverse proxy ของตัวเองที่เปลี่ยน Hostเปิด trust-proxy ไม่อย่างนั้นการบันทึกในหน้า admin จะได้ 403 (0.64)
เรียก GET <api>/jobs/run ด้วย cookie ของ adminใช้ POST หรือ cron secret (0.62)
มี cronSecret สั้นกว่า 32 ตัวอักษรทำให้ยาวขึ้น ไม่อย่างนั้นแอปไม่เริ่มทำงาน (0.62)
ส่งอีเมลผ่าน SMTP พอร์ต 587 ที่ไม่มี TLSตั้ง requireTLS: false (0.65)
อ่าน schema ของ GraphQL จาก productionตั้ง introspection: true (0.65)
รับเก็บเงินปลายทางด้วย manualAdapterตั้ง expiresIn: false ไม่อย่างนั้นคำสั่งซื้อที่ไม่จ่ายจะถูกยกเลิกหลัง 3 วัน (0.65)
เรียก POST <api>/shop/confirm โดยไม่ใช้ client ของร้านส่งตะกร้าของผู้เยี่ยมชมไปด้วย (0.63)
มี unique ใน field ที่อยู่ใน group, array หรือ blockที่นั่นไม่เคยถูกบังคับใช้ ให้ย้ายขึ้นระดับบนสุดหรือตรวจใน hook (0.66)
รับเงินผ่าน Stripeตั้ง API version ของ webhook endpoint เป็น 2026-09-30.endive หรือตั้ง apiVersion (0.67)

ขั้นตอน

  1. Backup ฐานข้อมูลและไฟล์อัปโหลด (Backup) migration เดินหน้าได้อย่างเดียว
  2. อัปเกรดทุกแพ็กเกจพร้อมกัน บนเครื่อง dev ก่อน:
npm
npm install @easy-cms/core@latest easy-cms@latest
pnpm
pnpm add @easy-cms/core@latest easy-cms@latest
yarn
yarn add @easy-cms/core@latest easy-cms@latest
bun
bun add @easy-cms/core@latest easy-cms@latest

ใส่แพ็กเกจ @easy-cms/* อื่นที่แอปใช้ในบรรทัดเดียวกัน 3. เริ่มแอป แล้วแก้ตามที่การตรวจ config บอก จนแอปเริ่มได้โดยไม่มี error คำเตือนของมัน (option ที่ไม่รู้จัก, unique ใน group…) ก็ควรอ่าน 4. หาชื่อเก่าในโค้ด: รัน test ด้วย NODE_OPTIONS=--throw-deprecation สิ่งที่ยังใช้ route หรือคำสั่งที่เลิกใช้จะล้มให้เห็นชัด แล้วไล่ดูเช็คลิสต์ 5. สร้าง migration: npx easy-cms migrate:create upgrade release อาจเพิ่มตารางหรือคอลัมน์ภายใน ตรวจ SQL แล้ว commit 6. ลองกับสำเนาข้อมูล production: restore backup ลงฐานข้อมูลทดลอง รัน npx easy-cms migrate เริ่มแอป แล้วเปิดหน้า admin 7. Deploy: backup รัน easy-cms migrate แล้วเริ่มเวอร์ชันใหม่ ดูคำเตือนใน log ของ server ช่วง request แรกๆ

ทุกการเปลี่ยนแปลงถูกทดสอบการอัปเกรดจากฐานข้อมูลที่สร้างด้วย release ตั้งแต่ 0.10 ทั้งบน SQLite และ Postgres

การย้อนกลับ: restore backup ที่ทำไว้ก่อน deploy แล้ว deploy แอปเวอร์ชันก่อนหน้า

หัวข้อด้านล่างแสดงการเปลี่ยนแปลงของแต่ละ release

0.68

  • GraphQL ต้องมี graphql ในโปรเจกต์: @easy-cms/plugin-graphql ไม่ติดตั้ง graphql ชุดของตัวเองแล้ว plugin กับโค้ด extend ของคุณจึงใช้ชุดเดียวกัน npm, pnpm และ Bun ติดตั้งให้เอง ถ้าใช้ Yarn ให้รัน yarn add graphql ใช้ได้ทั้งเวอร์ชัน 16 และ 17

0.67

  • ร้านค้าที่ใช้ Stripe: adapter ขอ Stripe API version 2026-09-30.endive ไม่ว่าติดตั้งแพ็กเกจ stripe เวอร์ชันไหน (17 ถึง 23) ให้สร้าง webhook endpoint ด้วยเวอร์ชันนี้ หรือตั้ง stripeAdapter({ apiVersion })

0.66

  • unique ใน group, array หรือ block ขึ้นคำเตือน: ที่นั่นไม่เคยถูกบังคับใช้อยู่แล้ว ให้ย้าย field ขึ้นระดับบนสุด หรือตรวจใน hook แทน
  • Multi-tenant: staff ของ tenant เห็นการเป็นสมาชิกของคนอื่นเฉพาะใน tenant นั้น การตรวจ audit log (POST <api>/admin/audit/verify) ทำได้เฉพาะ admin ที่เข้าได้ทุก tenant และ collection ของ plugin ที่ใส่แยกตาม tenant ไม่ครบจะขึ้นคำเตือน

0.65

  • ร้านค้า: คำสั่งซื้อแบบโอนเงินที่ไม่จ่ายจะถูกยกเลิกหลัง 3 วัน และคืนสต็อก (manualAdapter({ expiresIn }) เป็นวินาที หรือ false เพื่อเก็บไว้ เช่น เก็บเงินปลายทาง) คำสั่งซื้อที่สร้างก่อนอัปเกรดไม่ถูกยกเลิก คำสั่งซื้อมี field expiresAt เพิ่ม บน production ให้รัน easy-cms migrate:create แล้ว deploy migration
  • GraphQL introspection ปิดใน production เป็นค่าเริ่มต้น ตั้ง introspection: true ใน graphqlPlugin() ถ้ายังต้องการ ส่วน easy-cms generate:graphql ยังเขียน schema ให้ codegen ได้เหมือนเดิม ดู GraphQL
  • SMTP บน port 587 ต้องใช้ STARTTLS ถ้า server ไม่มี TLS เช่น mail catcher ในเครื่อง ให้ตั้ง smtp({ requireTLS: false }) ดู อีเมล
  • เข้ารหัส backup ได้ ด้วย backups.encryptionKey (ไม่บังคับ) ถอดรหัสไฟล์เองด้วย easy-cms backup:decrypt ดู Backup
  • ถ้าไม่เปิด audit log การ login ไม่สำเร็จจะถูกเขียนลง log ของ server เป็นคำเตือน
  • ฟอร์มที่ไม่รู้ IP ของ client ใช้ rate limit ร่วมกันหนึ่งชุดที่ใหญ่ขึ้น (สิบเท่าของ max) แทนที่จะไม่มีเลย

0.64

  • cookie ชื่อ __Host-ecms-session, __Host-ecms-csrf และ __Host-ecms-sso เมื่อใช้ HTTPS cookie ชื่อเดิมยังอ่านได้ จึงไม่มีใครถูก logout
  • ถ้าอยู่หลัง reverse proxy ของคุณเองที่เปลี่ยน Host ให้เปิด trust proxy (trustProxy หรือ --trust-proxy สำหรับ standalone) การตรวจ origin ของ CSRF ใช้ X-Forwarded-Host เฉพาะตอนนั้น ถ้าไม่เปิด การเขียนข้อมูลจากหน้า admin อาจล้มเหลวด้วย 403 ส่วน Vercel และ Netlify ไม่ต้องตั้งอะไร ดู ความปลอดภัย
  • บน production API และหน้า admin ส่ง Strict-Transport-Security (HSTS 1 ปี) ให้ redirect HTTP ไป HTTPS ที่ proxy หรือโฮสต์
  • sitemap และ robots.txt ของ SEO plugin ใช้ serverURL สร้างที่อยู่ของตัวเองแทน host ของ request ควรตั้ง serverURL บน production

0.63

  • การแก้ไขหรือลบเอกสารที่ผู้ใช้อ่านไม่ได้ตอบ 404 แทน 403
  • ลิงก์ตัวอย่างจากหน้า admin อ่านด้วยสิทธิ์ของคนที่สร้าง ส่วน createPreviewToken รับ { user } เพื่อให้ทำงานแบบเดียวกัน
  • ร้านค้า: POST <api>/shop/confirm ต้องมาจากลูกค้าของการชำระเงินนั้น หรือผู้เยี่ยมชมที่ส่งตะกร้ามาด้วย (cart: { id, secret }) client ส่งให้อยู่แล้ว ส่วนหน้าที่เรียก endpoint เองต้องส่งด้วย

0.62

  • ไม่รับรหัสผ่านที่ใช้บ่อย: การตั้งรหัสผ่านที่อยู่ในรายการราว 2,000 อันที่ใช้บ่อยที่สุด (password1, iloveyou…) จะได้ validation error ส่วนรหัสผ่านที่ตั้งไว้แล้วยังใช้ได้
  • cronSecret ต้องยาวอย่างน้อย 32 ตัวอักษร เหมือน secret: ถ้าใน config สั้นกว่านั้น แอปจะไม่เริ่มทำงาน ส่วน CRON_SECRET ที่สั้นจะเป็นคำเตือน สร้างได้ด้วย openssl rand -hex 32
  • <api>/jobs/run ที่ใช้ session cookie รับเฉพาะ POST (GET ได้ 405) ส่วน cron ที่ส่ง Authorization: Bearer <cronSecret> ยังใช้ GET ได้

0.60

0.60 ล็อกชื่อและรูปร่างที่จะใช้ตลอด 1.x แอปส่วนใหญ่แก้แค่ config (error บอกทีละข้อ) และชื่อเมธอดของ Local API ที่เปลี่ยน ถ้าเรียกใช้อยู่

Config

เดิมใหม่
admin.siteUrladmin.siteURL
admin.menuadmin.order ในแต่ละ collection, global และ page
icon, editIn ของ collection หรือ globaladmin.icon, admin.editIn
position ของ fieldadmin.position
admin.defaultValue ของ fieldadmin.initialValue
to ใน admin.commandshref
commandscliCommands
auth.allowSignUpauth.providerSignUp
auth.members.signupauth.members.signUp
audit.keepaudit.keepDays
audit.scope(context)audit.scope({ context, user })
versions.maxversions.keep
backups.every: 'day' | 'week'backups.frequency: 'daily' | 'weekly'
localization: null, admin.switcher: nullfalse
notEquals ใน admin.conditionnot_equals เหมือน where (มี not_in ด้วย)

hook ได้ previousDoc (เดิมเป็น originalDoc ใน beforeValidate และ beforeChange) option ที่ Easy CMS ไม่รู้จักจะขึ้นเป็นคำเตือน พร้อมชื่อที่ใกล้เคียงที่สุด

พฤติกรรมอื่นที่เปลี่ยน:

  • Webhook ไม่ส่ง event ของ users อีกต่อไป เว้นแต่ webhook นั้นใส่ users ใน collections
  • access ของ field มี create ถ้าไม่ตั้ง จะใช้ update ตอนสร้างเหมือนเดิม
  • global มี hook beforeValidate และ admin.group: false ซ่อน collection หรือ global จากเมนู

Local API และ access

เดิมใหม่
isLoggedInisStaff (isSignedIn เหมือนเดิม)
cms.scheduled()cms.findSchedule()
cms.scheduledGlobal()cms.findGlobalSchedule()
cms.upcomingJobs()cms.upcomingSchedules()
type ScheduledJobScheduledPublish
storage localStorage()diskStorage() (DiskStorageOptions)
  • isAdmin ไม่นับ admin ของบางส่วนของเว็บอีกต่อไป (ผู้ใช้ที่มี scoped: true เช่น admin ของ tenant เดียว) ถ้าเคยพึ่งพฤติกรรมนี้ ให้ใช้ isSystemAdmin หรือเขียนกฎเอง
  • cms.runJobs() (และ GET <api>/jobs/run) ตอบ { scheduled: { ran, failed }, jobs, webhooks, emails }
  • cms.increment() throw NotFoundError เมื่อไม่มีเอกสารนั้น ส่วน null หมายถึงค่าจะเกินขอบเขตเท่านั้น
  • cms.audit.list({ ...filter, page, limit }) ตอบแบบเดียวกับ find: { docs, totalDocs, page, … }
  • helper ที่ใช้กันเฉพาะในแพ็กเกจ @easy-cms/* ย้ายไป @easy-cms/core/internal ซึ่งไม่อยู่ใต้ semver ให้ import จาก @easy-cms/core (หรือ @easy-cms/core/plugin) เท่านั้น

Error

EasyCMSError ทุกตัวมี code และ error ของ REST ส่งมาด้วยใน errors[].code เช่น VALIDATION_ERROR, UNAUTHORIZED, NOT_FOUND, TOO_MANY_REQUESTS GraphQL ใช้รหัสชุดเดียวกัน ให้เช็ค code แทนข้อความ

  • ConfigError เป็น EasyCMSError (CONFIG_ERROR)
  • คำตอบ 429 มี Retry-After
  • คำขอที่เริ่ม session (login, first register, reset password) ตอบ 200
  • การยกเลิกการเผยแพร่ตามเวลาตอบ { deleted: 1 }

Route ของ REST

เฉพาะแอปที่เรียก REST API เอง

  • การ sign in ย้ายไป <api>/auth/<action>: login, logout, me, init, signup, verify-email, forgot-password, reset-password, first-register ส่วน path เดิม <api>/users/<action> ยังใช้ได้ตลอด 1.x
  • อัปเดต global ด้วย PATCH (POST ยังใช้ได้)
  • ?fallbackLocale= (?fallback-locale= ยังใช้ได้)
  • สิ่งที่หน้า admin ใช้อย่างเดียวย้ายไป <api>/admin/ui/* ซึ่งไม่อยู่ใต้ semver อย่าเรียกจากแอป
  • endpoint ของ plugin ที่ path ตรงกับของ API แต่ method ต่างกัน จะปล่อย method นั้นให้ API ตอบ

CLI

เดิมใหม่
easy-cms create-admineasy-cms admin:create
easy-cms run-scheduledeasy-cms jobs:run

ชื่อเดิมยังใช้ได้ คำสั่งในตัวไม่รับ option ที่ไม่รู้จัก พิมพ์ผิดอย่าง --outt จะเป็น error แทนที่จะถูกข้ามไปเงียบๆ

Plugin

option ที่เปลี่ยนชื่อ ถ้าใช้ชื่อเดิม แอปจะไม่เริ่มทำงานและบอกชื่อใหม่

Pluginเดิมใหม่
SEOsiteUrlsiteURL
ฟอร์มfieldsfieldKinds
ฟอร์มminSubmitTime (ms, 2000)minSubmitSeconds (2)
ฟอร์มfetchถอดออก
Multi-tenanttenantsSlugslugs: { tenants }
Multi-tenantfindTenantFor(cms, by, slug), tenantContext(cms, by, slug){ …by, collection: slug }
Redirectsslugslugs: { redirects }
RedirectscacheTTL (ms, 60000)cacheMaxAge (วินาที, 60)
Nested docsfieldsfieldNames
GraphQLexcludecollections และ globals: slug ที่อยู่ใน schema (ค่าเริ่มต้นคือทั้งหมด)
ร้านค้าcustomers.signup, signup()customers.signUp, signUp()

type: FormBuilderOptions, MultiTenantOptions และ EcommerceOptions เปลี่ยนเป็น FormBuilderPluginOptions, MultiTenantPluginOptions และ EcommercePluginOptions

Adapter:

Adapterเดิมใหม่
sqlite()busyTimeoutbusyTimeoutMs
s3Storage()publicUrlpublicURL

การเขียน plugin

  • import definePlugin, defineFieldType, type ของ plugin และคลาส error จาก @easy-cms/core/plugin ซึ่งไม่มีโค้ดฝั่ง server จึงให้ element ของ admin import ได้ด้วย
  • ระบุ plugin API ที่เขียนไว้: definePlugin(fn, { name, version, apiVersion: 1 }) plugin ที่เขียนสำหรับ เวอร์ชันอื่นจะหยุดการเริ่มทำงานและบอกว่าต้องอัปเกรดอะไร adapter ตั้ง apiVersion: 1 ได้เหมือนกัน
  • onRequest เป็น list ได้ plugin ที่เพิ่ม onRequest ควรต่อท้าย list แทนการเรียกฟังก์ชันเดิมเอง แต่ละตัวเห็น user ที่ตัวก่อนหน้าคืนมา และ context ถูกรวมกัน
  • cliCommands ได้ flags: --dry-run คือ flags.dryRun, --limit=5 คือ flags.limit
  • tag ของ element ใน admin เป็นชื่อ custom element ใดก็ได้ที่มีขีด ไม่ต้องขึ้นต้นด้วย ecms-
  • widget ของ dashboard มี id ซึ่ง role ใช้แทน tag
  • warnDeprecated(code, message) ขึ้นคำเตือนการเลิกใช้ครั้งเดียวต่อ process สำหรับ option ของคุณที่เปลี่ยนชื่อ ดูเวอร์ชันและการเลิกใช้

Next.js: createRouteHandlers(config, options) รับ basePath และ getClientIp นอกจาก trustProxy