การอัปเกรด
สิ่งที่จะได้เรียนรู้
แต่ละเวอร์ชันเปลี่ยนอะไรไปบ้าง และต้องแก้อะไรในแอปเมื่ออัปเกรด
ก่อนหน้านี้: การตั้งค่า
อัปเกรดแพ็กเกจ @easy-cms/* และ easy-cms ทุกตัวพร้อมกัน เพราะออกเวอร์ชันด้วยเลขเดียวกัน
npm install @easy-cms/core@latest easy-cms@latestpnpm add @easy-cms/core@latest easy-cms@latestyarn add @easy-cms/core@latest easy-cms@latestbun 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 หลังจากนั้นเพิ่มความปลอดภัย แอปที่ยังอยู่ 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
เช็คลิสต์: การเปลี่ยนแปลงที่ไม่ทำให้แอปหยุด
| ถ้าแอปของคุณ… | ให้ทำ |
|---|---|
พึ่ง 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) |
ขั้นตอน
- Backup ฐานข้อมูลและไฟล์อัปโหลด (Backup) migration เดินหน้าได้อย่างเดียว
- อัปเกรดทุกแพ็กเกจพร้อมกัน บนเครื่อง dev ก่อน:ใส่แพ็กเกจsh
npm install @easy-cms/core@latest easy-cms@latestshpnpm add @easy-cms/core@latest easy-cms@latestshyarn add @easy-cms/core@latest easy-cms@latestshbun add @easy-cms/core@latest easy-cms@latest@easy-cms/*อื่นที่แอปใช้ในบรรทัดเดียวกัน - เริ่มแอป แล้วแก้ตามที่การตรวจ config บอก จนแอปเริ่มได้โดยไม่มี error คำเตือนของมัน (option ที่ไม่รู้จัก,
uniqueใน group…) ก็ควรอ่าน - หาชื่อเก่าในโค้ด: รัน test ด้วย
NODE_OPTIONS=--throw-deprecationสิ่งที่ยังใช้ route หรือคำสั่งที่เลิกใช้จะล้มให้เห็นชัด แล้วไล่ดูเช็คลิสต์ - สร้าง migration:
npx easy-cms migrate:create upgraderelease อาจเพิ่มตารางหรือคอลัมน์ภายใน ตรวจ SQL แล้ว commit - ลองกับสำเนาข้อมูล production: restore backup ลงฐานข้อมูลทดลอง รัน
npx easy-cms migrateเริ่มแอป แล้วเปิดหน้า admin - Deploy: backup รัน
easy-cms migrateแล้วเริ่มเวอร์ชันใหม่ ดูคำเตือนใน log ของ server ช่วง request แรกๆ
ทุกการเปลี่ยนแปลงถูกทดสอบการอัปเกรดจากฐานข้อมูลที่สร้างด้วย release ตั้งแต่ 0.10 ทั้งบน SQLite และ Postgres
การย้อนกลับ: restore backup ที่ทำไว้ก่อน deploy แล้ว deploy แอปเวอร์ชันก่อนหน้า
หัวข้อด้านล่างแสดงการเปลี่ยนแปลงของแต่ละ release
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เพื่อเก็บไว้ เช่น เก็บเงินปลายทาง) คำสั่งซื้อที่สร้างก่อนอัปเกรดไม่ถูกยกเลิก คำสั่งซื้อมี fieldexpiresAtเพิ่ม บน 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.siteUrl | admin.siteURL |
admin.menu | admin.order ในแต่ละ collection, global และ page |
icon, editIn ของ collection หรือ global | admin.icon, admin.editIn |
position ของ field | admin.position |
admin.defaultValue ของ field | admin.initialValue |
to ใน admin.commands | href |
commands | cliCommands |
auth.allowSignUp | auth.providerSignUp |
auth.members.signup | auth.members.signUp |
audit.keep | audit.keepDays |
audit.scope(context) | audit.scope({ context, user }) |
versions.max | versions.keep |
backups.every: 'day' | 'week' | backups.frequency: 'daily' | 'weekly' |
localization: null, admin.switcher: null | false |
notEquals ใน admin.condition | not_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
| เดิม | ใหม่ |
|---|---|
isLoggedIn | isStaff (isSignedIn เหมือนเดิม) |
cms.scheduled() | cms.findSchedule() |
cms.scheduledGlobal() | cms.findGlobalSchedule() |
cms.upcomingJobs() | cms.upcomingSchedules() |
type ScheduledJob | ScheduledPublish |
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()throwNotFoundErrorเมื่อไม่มีเอกสารนั้น ส่วน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-admin | easy-cms admin:create |
easy-cms run-scheduled | easy-cms jobs:run |
ชื่อเดิมยังใช้ได้ คำสั่งในตัวไม่รับ option ที่ไม่รู้จัก พิมพ์ผิดอย่าง --outt จะเป็น error แทนที่จะถูกข้ามไปเงียบๆ
Plugin
option ที่เปลี่ยนชื่อ ถ้าใช้ชื่อเดิม แอปจะไม่เริ่มทำงานและบอกชื่อใหม่
| Plugin | เดิม | ใหม่ |
|---|---|---|
| SEO | siteUrl | siteURL |
| ฟอร์ม | fields | fieldKinds |
| ฟอร์ม | minSubmitTime (ms, 2000) | minSubmitSeconds (2) |
| ฟอร์ม | fetch | ถอดออก |
| Multi-tenant | tenantsSlug | slugs: { tenants } |
| Multi-tenant | findTenantFor(cms, by, slug), tenantContext(cms, by, slug) | { …by, collection: slug } |
| Redirects | slug | slugs: { redirects } |
| Redirects | cacheTTL (ms, 60000) | cacheMaxAge (วินาที, 60) |
| Nested docs | fields | fieldNames |
| GraphQL | exclude | collections และ globals: slug ที่อยู่ใน schema (ค่าเริ่มต้นคือทั้งหมด) |
| ร้านค้า | customers.signup, signup() | customers.signUp, signUp() |
type: FormBuilderOptions, MultiTenantOptions และ EcommerceOptions เปลี่ยนเป็น FormBuilderPluginOptions, MultiTenantPluginOptions และ EcommercePluginOptions
Adapter:
| Adapter | เดิม | ใหม่ |
|---|---|---|
sqlite() | busyTimeout | busyTimeoutMs |
s3Storage() | publicUrl | publicURL |
การเขียน 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