2023年3月15日 星期三

Typescript在filter後需要指定過濾完的type

 ts畢竟屬於一種擴充的超集合,並非是原生的一個語言,所以某些情境使用上會沒有這麼直覺。

先不要戰!!還是都有得解,只是不熟悉的小夥伴可能需要去查閱一下資料。

例如大家最熟悉的一個方法filter,對ts來說他就沒有辦法很明確知道過濾完之後會剩下什麼類別的集合,參見圖片


ide提示sub有可能是undefined,但理論上我應該在filter就把sub為空的都過濾掉了。當然你可以忽略紅線,也可以使用?或!告訴ide一定有值,今天要使用我上網爬文看到的方法,只要加上一段定義回傳內容即可


etc,如果直接定義方法回傳內容為(x): iType => ...是會報錯的喔,因為!!x.sub是boolean並非iType

2023年3月14日 星期二

AdonisJs第九天Provider

前一篇有稍微提到Provider,這個功能,今天在稍微深入講一下。 

Provider可以為 AdonisJS 應用程式提供許多不同的功能。AdonisJS 也提供了許多內置的 Provider,同時也允許開發人員創建自己的 Provider。

Provider需要實現 register ready boot shutdown四個生命週期。register 方法用於註冊 container,而 boot 方法則用於container已準備好的情況,ready shutdown上面都有各自的註解,目前小編也沒有什麼使用範例及情境就先暫時忽略 。

以下是一個簡單的 Provider 範例,在預設情況下,在創立專案的同時會附有/provider/AppProvider.ts檔案在專案中以利擴充:

import { ApplicationContract } from "@ioc:Adonis/Core/Application"
export default class AppProvider {
constructor(protected app: ApplicationContract) {}

public register() {
// Register your own bindings
}

public async boot() {
// IoC container is ready
// await import("@ioc:Adonis/Lucid/Database")
global.DB = require("@ioc:Adonis/Lucid/Database")
DB.transactionFn = async <T>(fn): Promise<T> => {
const trx = await DB.transaction()
let res
try {
res = (await fn(trx)) as T
await trx.commit()
} catch (e) {
await trx.rollback()
switch (e.name) {
case "ApiException":
case "E_VALIDATION_FAILURE":
throw e
default:
throw new ApiException(CommonCodes.SQL_ERROR, e.message)
}
}
return res
}
App.use("Adonis/Core/Event").on("db:query", (query) => {
DB.prettyPrint(query)
})
}

public async ready() {
// App is ready
}

public async shutdown() {
// Cleanup, since app is going down
}

在上面這個範例中我註冊了一個全域的變數,並把這個變數擴充了一個新的方法(transaction),另外又偵聽一個事件,只要有db操作,就會把sql打印出來。

這邊有一個值得注意的點是,在AppProvider觸發的時候,有些套件世上未初始化的,所以無法透過import方式引入,變得需要在boot時,用await import或require去引入套件,這是一個小雷。

在設置全域變數,ts當然也要做出相對應的設定,才不會在使用時看到一堆紅線,找到/global.d.ts的檔案(沒有責自己創建),輸入以下

import { DatabaseContract } from "@ioc:Adonis/Lucid/Database"
declare global {
var DB: DatabaseContract
}

當然小編不建議隨便把東西設為全域變數,這只是紀錄一下之前年少做過不懂事,踩過的坑。

最後還需檢查/.adonisrc.json裏面providers是否已經註冊了剛剛所編寫的AppProvider

{
"typescript": true,
......
"providers": [
"./providers/AppProvider", // here
......
]
}

當這一切準備好就可以進到使用的環節了

import { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"
import { inject } from "@adonisjs/fold"
import TestService from "../Service/TestService"
import DeleteValidator from "../Validators/DeleteValidator"

@inject()
export default class TestController {
constructor(private service: TestService) {}

async delete({ request }: HttpContextContract) {
const { id } = await request.classValidate(DeleteValidator)
return DB.transactionFn((trx) => this.service.delete({ id, trx }))
}
}

這樣就大功告成拉。

2023年3月12日 星期日

AdonisJs第八天自動注入

 依賴注入可以說是laravel裡非常重要的特性,實踐出來的機制稱之為容器,或是ioc,adonisjs也有實踐這個功能,然而adonisjs實踐的可不只有容器,同時還有自動注入。只是上面兩個功能,在官網上幾乎都是一筆帶過,甚至自動注入官方網站根本就沒有提到,要不是去看4版的文件,不然就是去參考laravel的文件,再不就是去問狗了。今天再就此作一個紀錄。

adonisjs的container被包在Application這個全域變數當中,可以隨時調用

import Application from '@ioc:Adonis/Core/Application'

export default class TestController {
async test({ request }: HttpContextContract) {
Application.container
}
}

官方做法是建議在AppProvider.ts裡,將所有會用到的物件都加以宣告,要使用時就可以直接呼叫相對應的key

// 註冊如下
export default class AppProvider {
constructor(protected app: ApplicationContract) {}

public register() {
this.app.container.bind("test", () => new Object())
}
}
// 使用如下
import Application from '@ioc:Adonis/Core/Application'

export default class TestController {
async test({ request }: HttpContextContract) {
const object = Application.container.make("test")
}
}

這個好處是降低物件之間的依賴,同時減少new複雜物件的重工。這網路都很多教學我就不再贅述,有興趣可以自行去查關鍵字"依賴注入"或"容器"。

然而每個要使用的物件service, repository, tools全部都要註冊這會不會太麻煩了一點。這會導致AppProvider.ts過於龐大,一日小編在網路查找資訊時看到了曙光。

import { inject } from "@adonisjs/fold"

@inject()
export default class TestController {
constructor(private service: TestService) {}

async test({ request }: HttpContextContract) {
return this.service.get({})
}
}

adonisjs使用了ts的decorator,撰寫了inject方法,使用方式很簡單,源碼也不長,可以使用在class或method上,參數可打給不打(小編用到目前還沒給過),使用在class上就會把建構子上宣告的變數,依照類別自動注入,非常快速及方便。唯一要注意的是,被注入的物件上層,建議也使用inject decorator,不然宣告起來會很費事,範利如下

import { inject } from "@adonisjs/fold"
@inject()
export default class TestService {
constructor(private repo: TestRepository) {}

get({ }) {
return this.repo.get()
}
}
// 如果不使用inject情境
export default class TestController {
constructor(private service: TestService) {
this.service = new TestService(new TestRepository())
}

async test({ request }: HttpContextContract) {
return this.service.get({})
}
}

這還是只有兩層的情況下,如果TestRepository的建構子也有參數,那你還是乖乖使用inject完事吧。

只要container跟inject,基本上可以說是想用什麼就自動注入什麼,相依性可以說大大降低。可以說在專案上幾乎沒有container出馬的份,inject就可以完成所有工作。但container當然還是有他的市場,adonisjs改寫過轉譯器,我們常常在套件中看到@ios:開頭的套件,例如

import Application from '@ioc:Adonis/Core/Application'

這當然不是這個套件真實的名稱跟路徑,是因為轉譯器的效果。只要發現該抬頭的字串,轉譯器就會自動去尋找container所註冊過的同名物件,我們舉昨天class-validator為例子

// 實際註冊名稱如下
export default class ClassValidatorProvider {
constructor(protected app: ApplicationContract) {}

public async boot() {
this.bindClassValidator();
}

private bindClassValidator() {
const adonisValidator = this.app.container.use("Adonis/Core/Validator");
// 註冊名稱為 Adonis/ClassValidator
this.app.container.singleton("Adonis/ClassValidator", () => {
return {
validate: require("../src").validate,
...adonisValidator,
};
});
}
}

// 專案經由npm install後並掛載provider,呼叫方式如下
import { validate } from "@ioc:Adonis/ClassValidator"

轉譯器讓import也可以直接把container所註冊的所有物件,不用透過專案內的container也可以呼叫使用,這實在是非常聰明且方便的做法,也有降低耦合的效果。不過這缺點就是必須是npm套件,所以專案內還是用inject就好吧


3/25更新

我在某個教學網站上看到,其實可以不用上到npm,直接本地provider bind完就可以在專案中直接使用,至於有沒有成功那就留給大家有空去驗證了

參考網站:https://adocasts.com/lessons/service-providers-and-the-ioc-container


4/28更新

更多使用情境可以參考
https://dev.to/serjoagronovdev/how-to-inject-services-in-adonisjs-v5-constructor-method-2gge

2023年3月10日 星期五

AdonisJs第七天validate

 參數驗證算是後端功能中非常重要的一環,可以過濾前端算計來的參數,也可以轉換成適當的類型。

AdonisJs中有內建validate,使用方式先依照規範把validator建立起來,寫出要接收的schema以及相對應的訊息,之後加以驗證即可,範例如下

export default class BasicValidator {
constructor(protected ctx: HttpContextContract) {}

public schema = schema.create({
name: schema.string(),
})

public messages = {
"name.required": "name is required",
"name.string": "name must be string",
}
}

// 使用方式
export default class BasicController {
async create({ request }: HttpContextContract) {
const { name } = await request.validate(BasicValidator)
return name
}
}

這邊值得一題,驗證完會回傳驗證結果,並轉成相對應的物件,例如string, number, DateTime, File......因此不會接收前端傳回來多餘的內容。如果設計得宜,是非常好的保障。

ect. 如果messages沒有相對應的定義訊息,系統就會使用預設訊息拋出

schema的規範相當多元,也可以上訂傳進來是物件,陣列,或是把時間格式轉換成DateTime,又或者限制輸入的內容金桔現在陣列之中的enum

public schema = schema.create({
date: schema.date({ format: 'yyyy-MM-dd'}), date格式必須為yyyy-MM-dd否則報錯
gender: schema.enum([1, 2]) // gender只能為1或2
object: schema.object().members({ a: schema.string(), b: schema.number() }),
array: schema.array().members(schema.number()),
})

validate也可以搭配rules使用,rules也預設有非常多的規則,甚至可以自定義驗證規則

public schema = schema.create({
id: schema.number([rules.exists({ table: "users", column: "id" })]), //id需存在於users table
})

更多細節都可以上官網查詢,這邊就不一一贅述。

接下來要介紹一個套件,如果你使用過nest js那你一定對他撰寫驗證的檔案驗證新穎,我同樣抱持著adonis會不會也這麼剛好有類似的東西開始搜索,(先說adonis提供的所有工具已足夠應付幾乎90%專案會遇上的問題,但小編我們身為攻城屍要足夠貪婪,能好就絕對不要濫,能省時間能更方便就絕對要去追求!!),終於讓我找到adonis-class-validateor,不過這套有點小bug,在File的轉換上會報錯,經小編修正後為@lu7766lu7766/adonis-class-validateor,安裝後下指令node ace invoke @lu7766lu7766/adonis-class-validateor設置好相關配置,即可開始使用。一樣先上範例

import { schema, validate } from "@ioc:Adonis/ClassValidator"
import { DateTime } from "luxon"

export default class GetValidator {
@validate(schema.date({ format: "yyyy-MM-dd" }), {
date: "date content error",
required: "date required",
format: "date format error",
})
public date: DateTime

@validate(schema.enum(["a", "b"])) // 使用預設message
public centre_code: string

@validate(schema.string.optional() // 可不送
public code?: string
}

// 使用方式
export default class BasicController {
// ...service 定義方式省略
async getList({ request }: HttpContextContract) {
const { date, centre_code, code } = await request.classValidate(GetValidator)
return this.service.getList({ date, centre_code, code })
}
}

使用class-validator有兩大好處。
第一個好處,邏輯較為集中,傳統設計分為schema,message兩大部分,一但參數多起來,在定義message時很常要滾輪往上看schema是如何設計,相當不便,使用class-validator可以一目瞭然。
第二個好處,因為小編習慣把所接收到的參數全部丟進service進行處理,在參數上類別用傳統的方式就必須一個一個重新定義,否則所有參數就會是any,無法發揮ts跟vscode的長處

getList({ date, centre_code, code }: {date: DateTime; centre_code: string; code?: string}) {
const start_day = date.startOf("day").toFormat("yyyy-MM-dd hh:mm:ss")
const end_day = date.endOf("day").toFormat("yyyy-MM-dd hh:mm:ss")
return return [start_day, end_day, centre_code, code]
}

如果使用class-validator就可以直接將參數類別定義為validator

getList({ date, centre_code, code }: GetValidator) {
const start_day = date.startOf("day").toFormat("yyyy-MM-dd hh:mm:ss")
const end_day = date.endOf("day").toFormat("yyyy-MM-dd hh:mm:ss")
return [start_day, end_day, centre_code, code]
}

簡單說就是善用ts的優勢,讓之後的操作上可以說會方便許多。

以上就是關於validate想要跟各位分享的內容。


3/25更新一下

@validate(schema.string())
public name: string

name會是必填沒錯,但卻沒辦法送出空字串,只是空字串應該也算是合法字串吧,小編認為這應該算是個bug,目前已在github上題出,看之後會不會得到作者回覆

3/26更新一下

由於我寄去的信被作者打槍了,他們一再強調這並非是bug而是有意為之,要我提出為什麼非得這麼做的“討論”。我哪有那美國時間跟他寫英文信在討論,難道要我告訴他,前端編輯文字,當把文字刪除還要特別處理為null這件事不會很不直覺嗎?這段話用中文講都不太好理解更何況要用英文,所以不得已之下先在前端把所有空字串改為null,再把後端設為nullable結束第一階段。

既然有第一階段當然會有第二階段,無奈下我把adonis/validation的原始檔下載回本地研究了一下,發現兩件事

// src/Schema/index.ts
// 可以看到schema.string的實作
function string(options?: { escape?: boolean; trim?: boolean } | Rule[], rules?: Rule[]) {
if (!rules && Array.isArray(options)) {
rules = options
options = {}
}
return getLiteralType('string', false, false, options, rules || []) as ReturnType<StringType>
}
string.optional = function optionalString(
options?: { escape?: boolean; trim?: boolean } | Rule[],
rules?: Rule[]
) {
if (!rules && Array.isArray(options)) {
rules = options
options = {}
}
return getLiteralType('string', true, false, options, rules || []) as ReturnType<
StringType['optional']
>
}
string.nullable = function nullableString(
options?: { escape?: boolean; trim?: boolean } | Rule[],
rules?: Rule[]
) {
if (!rules && Array.isArray(options)) {
rules = options
options = {}
}
return getLiteralType('string', false, true, options, rules || []) as ReturnType<
StringType['nullable']
>
}
string.nullableAndOptional = function nullableAndOptionalString(
options?: { escape?: boolean; trim?: boolean } | Rule[],
rules?: Rule[]
) {
if (!rules && Array.isArray(options)) {
rules = options
options = {}
}
return getLiteralType('string', true, true, options, rules || []) as ReturnType<
StringType['nullableAndOptional']
>
}
// src/utils.ts 看到實作的方法
export function getLiteralType(
subtype: string,
optional: boolean,
nullable: boolean,
ruleOptions: any,
rules: Rule[]
): { getTree(): SchemaLiteral } {
const subTypeRule = rules.find((rule) => rule.name === subtype)
const optionsTree = {}

return {
getTree() {
return {
type: 'literal' as const,
nullable,
optional,
subtype: subtype,
rules: ([] as Rule[])
.concat(optional ? [] : nullable ? [schemaRules.nullable()] : [schemaRules.required()])
.concat(subTypeRule ? [] : [schemaRules[subtype](ruleOptions)])
.concat(rules)
.map((rule) => compileRule('literal', subtype, rule, optionsTree)),
}
},
}
}

// ----------------------------------------
// 意思是
schema.string() // 其實等同於
schema.string.optional([rules.required()])

schema.string.nullable() // 其實等同於
schema.string.optional([rules.nullable()])

schema.string.optional() // 意指不加入任何條件,沒後綴跟nullable後綴差別只在有沒有幫你預加上rule而已

// 另外再科普一下
schema.string.nullableAndOptional() // 其實等於optional一點用也沒有,不知道為什麼要這樣設計


那既然是由rule決定是否需要輸入,那我當然可以透過自定rule來定義出必填,但又可以是空白這件事,這我們必須先來看一下required到底做了什麼判斷

// src/Compiler/Validators/existence/required.ts
export const required: SyncValidation = {
compile: wrapCompile(RULE_NAME, [], () => {
return {
allowUndefineds: true,
}
}),
validate(value, _, { errorReporter, pointer, arrayExpressionPointer }) {
if (!exists(value)) {
errorReporter.report(pointer, RULE_NAME, DEFAULT_MESSAGE, arrayExpressionPointer)
}
},
}
// src/Validator/helpers.ts
export function exists(value: any) {
return !!value || value === false || value === 0
}
就是把false成員抓出來而已,但這樣並不能說明為什麼不送參數會報錯,因為不送就不會被驗證了吧,那原因應該在其他地方,想必就是compile裡面的allowUndefineds這個參數吧!!
既然知道方法,那剩下就是看自定rule怎麼寫,只是我看了一下官網,不只寫法跟原始碼的寫法不一樣,官網甚至只有教怎麼寫validate,也沒有地方可以設allowUndefineds。這時就要大大讚嘆typescript了,我看了一下rule的類別說明,竟然還可以寫第三個參數compileFn,這怎麼有點眼熟,裡面要回傳一個物件,裡面正好有我要的參數
{
name: string;
async: boolean;
allowUndefineds: boolean;
compiledOptions: Options;
}
那就開始動工吧

import { validator } from "@ioc:Adonis/Core/Validator"

validator.rule(
"stringAllowEmpty",
(value, _, options) => {
if (typeof value !== "string") {
options.errorReporter.report(options.pointer, "stringAllowEmpty", "stringAllowEmpty validation failed", options.arrayExpressionPointer)
}
},
() => ({
allowUndefineds: true,
})
)

declare module "@ioc:Adonis/Core/Validator" {
interface Rules {
stringAllowEmpty(): Rule
}
}

// usage
schema.string.optional([rules.stringAllowEmpty()])
以上打完收工!!
事實證明,偶爾看點原始碼還是有必要的。

3/26更新一下

小編還是覺得要多寫一個rule很麻煩,看能否把原本schema.string跟schema.number直接改寫,一開始也規劃直接寫在@lu7766lu7766/adonis-class-validateor裡面,後來絕地還是單除拆出來變成一個單獨得套件好了,所以我又增加了一個repo @lu7766lu7766/adonis-schema-override,非常直白告訴你我要覆寫了。
// src/providers/SchemaOverrideProvider.ts

import { ApplicationContract } from "@ioc:Adonis/Core/Application"
import { Rule, SchemaLiteral } from "@ioc:Adonis/Core/Validator"

// 因為這個rule沒有要給別人使用,所以類別宣告直接寫在檔案裡,沒有放在adonis-typings
declare module "@ioc:Adonis/Core/Validator" {
interface Rules {
defined(): Rule
}
}

export default class ClassValidatorProvider {
constructor(protected app: ApplicationContract) {}

public async boot() {
this.addRules()
this.overrideStringSchema()
}

private addRules() {
const { validator } = this.app.container.use("Adonis/Core/Validator")
    // 因為string, number本身就會檢測內容類別,所以我只需判斷是否有非文字的類別即可
validator.rule(
"defined",
(value, _, options) => {
if (value === undefined || value === null) {
options.errorReporter.report(options.pointer, "required", "required validation failed", options.arrayExpressionPointer)
}
},
() => ({
allowUndefineds: true,
})
)
}

private overrideStringSchema() {
const { schema, rules } = this.app.container.use("Adonis/Core/Validator")
// 因為schema.string類型比較特殊,既是方法又有屬性,所以暫時只想到一次全部覆寫這種寫法,歡迎有更好建議
function myString(...args) {
let option = {}
let params: Rule[] = [rules.defined()]
if (args.length === 1) {
params = params.concat(args[0])
} else if (args.length === 2) {
option = args[0]
params = params.concat(args[1])
}
return schema.string.optional(option, params) as {
t: string
getTree: () => SchemaLiteral
}
}
myString.nullable = schema.string.nullable
myString.optional = schema.string.optional
myString.nullableAndOptional = schema.string.nullableAndOptional
schema.string = myString
}
// number的方法就參照上面再寫一次就好
}


以上就真的大功告成收工了

小記:因為schema.string() == schema.strion.optional([rules.required()]),因為rule.required()是大家共用的,所以不能改,不然會天下大亂,我想作者大概也誤會我的意思,我只是想個別做調整就像上面這樣,總之就是小編又一次拯救了世界

2023年3月7日 星期二

AdonisJs第六天,全域取得context

 在AdonisJs設計中,原本只有Cntroller Method可以取得context(簡寫ctx),但在某些特殊情境下需要取得ctx,且無法透過controller傳遞參數怎麼辦呢?

小編在官網上尋找答案只找到可以使用以下方法,但在實際運行卻什麼也取不到,程式碼如下:

import HttpContext from '@ioc:Adonis/Core/HttpContext'
class SomeService {
public async someOperation() {
const ctx = HttpContext.get()
}
}

應該是缺少一點東西,小編把get改成getOrFail,讓他把錯誤印出來,錯誤訊息顯示

E_INVALID_ALS_ACCESS: HTTP context is not available. Set "useAsyncLocalStorage" to true inside "config/app.ts" file

所以應該是少了設定,只是這設定要加在哪裡,官方文件上只有文字描述要加設定,卻沒有告訴你怎麼做,加在哪。好在config/app.ts裡面設定也不多,一個一個嘗試後是加在

export const http: ServerConfig = {
    ...... // 中間忽略
    useAsyncLocalStorage: true,
}

以下看個使用情境

import { BaseModel, column, beforeSave } from "@ioc:Adonis/Lucid/Orm"
import Hash from "@ioc:Adonis/Core/Hash"
import HttpContext from "@ioc:Adonis/Core/HttpContext"
export default class User extends BaseModel {
  @column({ serializeAs: null })
public password: string

@beforeSave()
public static async beforeSave(user: User) {
    // 當路由名稱為approve時,實際命名方法如下
    // Route.post('xxx', 'XxxController.method').as('approve')
if (HttpContext.get()?.route?.name == "approve") {
return
}
if (user.$dirty.password) {
user.password = await Hash.make(user.password)
}
}
}

基本上在user password改變的前提下都需要經過加密,但就是有某些特殊場景不用,但又懶得把邏輯拆出來到service見一次做一次,只好用這個訪法去完成。

另外還有一個解法,是當判斷

user.password.startsWith("$argon2")

但這有一些問題,例如加密的方法如果改用不是argon lib加密,開頭就會變,或是他就是需要加密在加密。這些往往會變成bug,不過也是要看需求拉。以上就是這次簡短分享,也算做個筆記。