ExposedでEnumを使ってみる
PostgreSQLをデータベースと使用しているときに、一部のカラムをEnumとして表現したい。
そのためにはまず、Enumを型として定義する必要があるので作成してみる。これはテーブルの定義よりも前にやっておく。
こんな感じ。
class PGEnum<T : Enum<T>>(enumTypeName: String, enumValue: T?) : PGobject() { init { value = enumValue?.name type = enumTypeName } }
カラムの値として受け取りたい値をenum class
を使って定義する。
enum class Service { INSTAGRAM, TWITTER, }
この二つを使って、テーブルを定義するとこんな感じになる。
第三引数と第四引数では、いわゆるfrom/to変換関数を渡している。
object Posts : UUIDTable() { val service = customEnumeration("service", "ServiceEnum", { value -> Service.valueOf(value as String) }, { PGEnum("ServiceEnum", it) }) }
SchemaUtils.create()
を呼び出すときにexec()
メソッドを呼び出して、CREATE TYPE
を実行することで、新しいデータ型を定義する。
CREATE TYPE
で指定するのは、customEnumeration()
の呼び出し時にsql
引数として渡した引数と同じ値。ここではServiceEnum
を指定している。
AS ENUM
で指定するのは、enum class
で定義した内容と同じ値。
val database = Database.connect( url = jdbcURL, driver = driverClassName, user = user, password = password ) transaction(database) { exec( "DO $$ BEGIN " + "CREATE TYPE ServiceEnum AS ENUM ('INSTAGRAM', 'TWITTER'); " + "EXCEPTION " + "WHEN duplicate_object THEN null; " + "END $$;" ) SchemaUtils.create(Posts) }
ここで、注意しないといけないのが例外処理を行なっている点。
はじめて実行するときは、データ型がまだ定義されていない状態なので次のように書いたとしてもエラーは発生しない。
exec("CREATE TYPE ServiceEnum AS ENUM ('INSTAGRAM', 'TWITTER');")
ところが、2回目以降になると次のようなエラーが発生してしまう。
Exception in thread "main" org.jetbrains.exposed.exceptions.ExposedSQLException: org.postgresql.util.PSQLException: ERROR: type "serviceenum" already exists
これはすでに存在するデータ型を作成しようとしたために発生した例外。
なので、上記のコードではEXCEPTION
を使って例外処理を行いエラーの発生を制御している。
参考
DataTypes · JetBrains/Exposed Wiki · GitHub
Check if a user-defined type already exists in PostgreSQL - Stack Overflow
Exposed(DAO/DSL) で PostgreSQL からあれこれ SELECT するサンプル・覚書 - インドカレーファンクラブ
KtorとExposedの環境でちょっとデータベースにシードさせたいときのTips
Ktorでデータベースを使用したいので、com.takagimeow.infrastructure.database.dao
にDatabaseFactory
を実装してみます。
package com.takagimeow.infrastructure.database.dao import com.takagimeow.infrastructure.database.models.Users import kotlinx.coroutines.* import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.* import org.jetbrains.exposed.sql.transactions.experimental.* object DatabaseFactory { fun init() { val driverClassName = "org.postgresql.Driver" val jdbcURL = "jdbc:postgresql://localhost:5432/serverside" val user = "takagimeow" val password = "takagimeow_pass" val database = Database.connect( url = jdbcURL, driver = driverClassName, user = user, password = password ) transaction(database) { SchemaUtils.create(Users) } } suspend fun <T> dbQuery(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } }
Application.ktで、このオブジェクトのinit()
メソッドを呼び出すことでデータベースへ接続を行います。
fun Application.module() { DatabaseFactory.init() ... }
そしてUsers
はDSLで定義します。
object Users : Table() { val id = integer("id").autoIncrement() val email = varchar("email", 128) val password = varchar("password", 128) val name = varchar("name", 128) val roleType = varchar("role_type", 128) override val primaryKey = PrimaryKey(id) }
このとき、ユーザーの作成に関するルートは公開したくないけども、初期データとしてユーザーレコードをテーブルに登録したい場合を考えてみました。
プロジェクトではKoinを使いたいのでUserDaoFacade
インターフェースを作成します。
package com.takagimeow.infrastructure.database.dao import com.takagimeow.infrastructure.database.models.* interface UserDAOFacade { suspend fun addNewUser(email: String, password: String, name: String): Unit }
そして、その実装であるUserDaoFacadeImpl
クラスを実装します。
package com.takagimeow.infrastructure.database.dao.impl import com.takagimeow.infrastructure.database.dao.UserDAOFacade import com.takagimeow.infrastructure.database.dao.DatabaseFactory.dbQuery import com.takagimeow.infrastructure.database.models.Users import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.* class UserDAOFacadeImpl : UserDAOFacade { override suspend fun addNewUser(email: String, password: String, name: String): Unit = dbQuery { val insertStatement = Users.insert { it[Users.email] = email it[Users.password] = password it[Users.name] = name it[roleType] = "USER" } insertStatement.resultedValues?.singleOrNull()?.let(::resultRowToUser) } }
実際は、各リポジトリでこのクラスを注入することでデータベースに対する操作を行なっていくのですが、今回はこのUserDAOFacadeImpl
を再利用してデータの挿入を行おうと思います。
com.takagimeow.infrastructure.database
パッケージにseed
パッケージを作成します。
このパッケージの中に、InserUser.kt
を作成します。
そしてmain()
を定義して、コードブロックの中でUserDaoFacadeImpl
クラスのaddNewUser()
を呼び出します。
package com.takagimeow.infrastructure.database.seed import com.takagimeow.infrastructure.database.dao.DatabaseFactory import com.takagimeow.infrastructure.database.dao.impl.UserDAOFacadeImpl suspend fun main() { DatabaseFactory.init() val dao = UserDAOFacadeImpl() dao.addNewUser( "takagimeow@example.com", "password", "takagi", ) }
あとは、IntelliJ IDEAで表示されているmain()
の横の実行ボタンを押すだけ。
ターミナルには次の様に表示されました。
2022-11-16 02:15:03.836 [DefaultDispatcher-worker-3] DEBUG Exposed - INSERT INTO users (email, "name", "password", role_type) VALUES ('takagimeow@example.com', 'takagi', 'password', 'USER') プロセスは終了コード 0 で終了しました
TablePlusを通してデータを確認してみました。ちゃんと登録されています。
Jetpack Composeでディープリンクを開くテストをしてみる
ディープリンクを開くには次の書式でコマンドを実行する。
adb shell am start -W -a android.intent.action.VIEW -d <ディープリンクのURI> <アプリのパッケージ名>
実際に入力するときは次の様な感じ。
adb shell am start -W -a android.intent.action.VIEW -d "testapp://home_route" com.example.testapplication
アプリのパッケージ名は、build.gradleのandroid.defaultConfig.applicationId
で確認することができる。
android { ... defaultConfig { applicationId "com.example.testapplication"
開きたいComposableのために、route
とdeepLinks
を定義する。testapp
というディープリンクのスキーマを表すSCHEME
は定数としてどこかで定義しておく。
object HomeDestination : TaNavigationDestination { override val route = "feedl_route" override val destination = "feed_destination" val deepLinks = listOf( navDeepLink { uriPattern = "$SCHEME://${route}" } ) }
navigation
内で呼び出されるcomposable
に対して先ほど定義したroute
とdeepLinks
を渡す。
fun NavGraphBuilder.homeGraph( onUpPress: () -> Unit = {}, ) { composable( route = HomeDestination.route, deepLinks = HomeDestination.deepLinks, ) { navBackStackEntry -> HomeRoute( onUpPress = onUpPress, ) } }
AndroidManifest.xmlにcomposable
に設定した内容に沿って<intent-filter>
を追加する。
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="testapp" android:host="home_route" /> </intent-filter>
参考
アプリ コンテンツ用のディープリンクを作成する | Android デベロッパー | Android Developers
Androidの通知に表示したいアイコンを用意する
Androidでは、アプリのアイコンと通知用のアイコンは異なる。 特に異なる部分はカラーの部分で、通知用のアイコンには基本的に色をつけることはできない。
Android 5.0以降では通知機能が大幅に変更されたため、アイコンのカラーチャンネルがすべて無視される様になったのが関係しているらしい。
通知用のアイコンを考える場合、二つの要素に切り分けて考える。
1つ目は色のないアイコン。これは白色と透明な部分で構成されたsvg形式のファイルだと思って良い。アプリのロゴなどの目立たせたい部分を透明にしておくといった感じ。
もう一つは通知用のアイコンに対して動的に適用したい色の部分で、これは通知の作成時にsetColor関数に16進数の値を渡すことで設定することができる。
たとえば、次の様なアイコンを通知用に使いたい場合を考えてみる。
これをそのまま使おうとすると、Android側で色のついた部分はすべて白色に変換されてしまうため次の様に表示されてしまう。
なので、色付きだった箇所や強調したい部分を透明になるように調節した上で通知アイコンとして渡す必要がある。
ちなみに、setColor関数で色を制御できるのはあくまで通知ドロワーに表示される通知に対しての話で、ステータスバーに表示される通知アイコンの色の制御はできないので注意する必要がある。
通知を作成する際に、NotificationCompat.Builderでset系のメソッドを呼び出すタイミングでsetColor()
メソッドを使って通知ドロワーに表示される際の色を指定する。
ここでは0xC0B0D7
(紫)を指定している。
var builder = NotificationCompat.Builder(this, channelId) .setSmallIcon(com.example.app.R.drawable.ic_notification) .setColor(0xC0B0D7) .setContentTitle(contentTitle) .setContentText(contentText) .setPriority(NotificationCompat.PRIORITY_MAX) .setContentIntent(pendingIntent) .setAutoCancel(true) .build()
通知ドロワーでの表示は次の様な感じになる。
通知用のアイコンは、Android Studio上でResource Managerを開いて、プラスボタンクリックし、Import Drawablesをメニューから選択することで登録することができる。
svgファイルのインポートが完了すると、res/drawable
にxmlファイルが作成されるので、それをsetSmallIcon()
の引数に指定する。
参考
Notifications Overview | Android Developers
通知の概要 | Android デベロッパー | Android Developers
https://proandroiddev.com/android-iconography-notification-3acd3e00d6ec
`FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`がGitHub Actionsで発生した場合の対処法
問題
次のようなエラーが発生してしまい、GitHub Actions上で実行していたテストがFAILED
してしまった。
<--- JS stacktrace ---> FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 1: 0xb02930 node::Abort() [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 2: 0xa18149 node::FatalError(char const*, char const*) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 3: 0xcdd16e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 4: 0xcdd4e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 5: 0xe94b55 [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 6: 0xe95636 [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 7: 0xea3b5e [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 8: 0xea45a0 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/opt/hostedtoolcache/node/16.17.0/x64/bin/node] 9: 0xea751e v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) ....
結論
NODE_OPTIONS: "--max-old-space-size=5120""
をenvに追加する
... - name: Run all the tests run: yarn test working-directory: ./path/into/app env: ... NODE_OPTIONS: "--max-old-space-size=5120" ...
参考
tscの代わりにBabelにトランスパイルをまかせてみた
モノリポでパッケージを作る際にtsc
の代わりにbabel
を使ってみたかったので、これを機に色々調べてみた。
参考にしたのはこのサイト。
Using Babel with TypeScript | Learn TypeScript
Babelってなんだ
Babelは主にECMAScript 2015+のコードを、古いブラウザや環境での後方互換性をもったバージョンのJavaScriptに変換するために使用されるツール。
Babelがtscに代わって使われる理由はJSXをJavaScriptに変換できるから。 tscは変換できない。なのでReactをプロジェクトで使っている場合はBabelが必要になる。
必要なパッケージのインストール
とりあえず、TypeScriptやデコレーターだったりを使いたいので、それらをトランスパイルするのに必要なパッケージをまとめてみた。
@babel/core
- Babelのコアとなるライブラリ
@babel/preset-env
- 最新のJavaScriptの機能を使いつつ、それらをサポートしていないブラウザをターゲットにするためのプラグインの集まり。
@babel/preset-typescript
- TypeScriptのコードをJavaScriptに変換するためのプログラインの集まり
@babel/cli
- Babelを実行するために使われるコマンドラインツール。
babel-plugin-transform-typescript-metadata
@babel/plugin-proposal-decorators
@babel/plugin-proposal-class-properties
- 静的なクラスプロパティとプロパティ初期化構文で宣言されたプロパティを変換するプラグイン
babel-plugin-module-resolver
tsc-alias
yarn add -D @babel/core @babel/preset-env @babel/preset-typescript @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators babel-plugin-module-resolver babel-plugin-transform-typescript-metadata
設定ファイルを作成する
.babelrc
というファイルをルートディレクトリに作成する。
中身は次のようにする。
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"], "plugins": [ "babel-plugin-transform-typescript-metadata", ["@babel/plugin-proposal-decorators", { "version": "legacy" }], "@babel/plugin-proposal-class-properties", [ "module-resolver", { "root": ["."], "alias": { "~": "./src" } } ], ] }
babel-plugin-transform-typescript-metadata
は@babel/plugin-proposal-decorators
よりも前に記述しないといけないので注意する。
トランスパイルはBabelに任せて、tsc
では型チェックのみを行いたいので、noEmit
はfalse
に、declaration
はtrue
に、そしてemitDeclarationOnly
をtrue
に設定する。
... "compilerOptions": { ... "outDir": "dist", ... "noEmit": false, // 型定義ファイルは出力したいのでfalse "declaration": true, // 型定義ファイルの生成を指示する "emitDeclarationOnly": true, // 型定義ファイルのみ生成する ... }, ...
outDir
はBabelの出力先と一致させておく。ここではdist
を設定する。
スクリプトを追加する
build
スクリプトとしてpackage.jsonに次のコマンドを追加する。
... "scripts": { "build": "tsc -p tsconfig.json -d && tsc-alias -p tsconfig.json && npx babel ./src --extensions '.ts,.tsx' --out-dir ./dist", ... } ...
--out-file dist/index.js
ではなく--out-dir ./dist
を設定することに注意する。
もし--out-file dist/index.js
を指定してしまうと、./src/index.ts
のみがトランスパイルされて、distディレクトリに出力される。たとえ./src/index.ts
内で./src/classes/Sample.ts
をインポートしていたとしてもdist/classes
にSample.js
が出力されることはない。
tsc-alias
を使わないと~/classes/...
のようなエイリアスパスがそのまま型定義ファイルに出力されてしまう。なのでそれを防ぐためにtsc
による型定義ファイルの出力後に、tsc-alias
を使って型定義ファイル中のエイリアスパスを相対パスに置き換える必要がある。
module-resolverの設定
tsconfig.jsonで次のようにエイリアスを設定している場合
... "paths": { "~/*": ["./src/*"], }, "baseUrl": ".", ...
module-resolver
の設定は次のようにする。
... [ "module-resolver", { "root": ["."], "alias": { "~": "./src" } } ], ...
まとめ
Reactなどのフレームワークを触り始めたばかりのころに、WebpackやらBabelやらという当時の自分にはわからない単語が多すぎたせいで、なぜか今でもBabelを敬遠してしまっていた。
今回改めて自分で設定ファイル書いたりプラグイン追加してみたりしたけど、案外難しいことはなかった。
やっぱり自分で使ってみるのが一番ですね。
参考
Using Babel with TypeScript | Learn TypeScript
TypeScript: Documentation - Using Babel with TypeScript
TypeScript で型を検査する|【React/Redux】カンバンボードを実装して Web フロントエンド上級者を目指そう!|Techpit
GitHub - microsoft/tsyringe: Lightweight dependency injection container for JavaScript/TypeScript
GitHub - tleunen/babel-plugin-module-resolver: Custom module resolver plugin for Babel
Next.js + TypeScript + デコレータ + reflect-metadata を動かす
GitHub - justkey007/tsc-alias: Replace alias paths with relative paths after typescript compilation
React Native製のアプリで受け取ったプッシュ通知をタップして特定の画面に遷移させる方法
目次
- FirebaseコンソールからServer Keyを取得する
- Server KeyをExpoのサーバーにアップロードする
- 必要なパッケージをインストールする
FirebaseコンソールからServer Keyを取得する
Expoが認証情報を使って開発者の代わりにExpoのサーバーからプッシュ通知を送信するためには、Server KeyをExpoにアップロードする必要がある。
このキーはFirebaseコンソールから確認することができる。
Firebase プロジェクトの設定ページにアクセスして、Cloud Messagingタブをクリックする。
Server KeyはCloud Messaging API (Legacy)でのみ利用可能なので、デフォルトでは無効になっています。なので3点メニューをクリックして、Google Cloud ConsoleでAPIを管理をクリックします。
Google CloudコンソールのCloud Messagingの詳細ページが開かれたら、有効にするボタンをクリックする。
有効になったら、自動的に次のようなページに遷移する。
Cloud Messagingタブのページに戻ったらCloud Messaging APIが有効になっているのが確認できる。その下にサーバーキーが表示されているのでコピーして控えておく。
Server KeyをExpoのサーバーにアップロードする
先ほど控えておいたServer KeyをExpoのサーバーにアップロードする。<your-token-here>
の部分を実際のトークンに置き換えて次のコマンドを実行する。
expo push:android:upload --api-key <your-token-here>
これでアプリをインストールしているユーザーは、プロジェクトの認証情報を使用してFCMから通知を受け取ることができるようになる。
必要なパッケージをインストールする
プッシュ通知に対するハンドリングを行うのに必要なパッケージは次の2つ。
expo-device
expo-notifications
expo install expo-device expo-notifications
プッシュ通知の送信に必要なトークンを取得するための関数を実装する
プッシュ通知の送信にはメッセージを送信する宛先(Expo Push Token)の情報が必要になる。
そのトークンは実機上でしか取得できないので、クライアント上でgetPermissionsAsync
とrequestPermissionsAsync
を呼び出して権限の可否を確認して、getExpoPushTokenAsync
を使って実際のトークンを取得する。
import * as Device from "expo-device"; import { Platform } from "react-native"; import * as Notifications from "expo-notifications"; export async function registerForPushNotifications() { let token = ""; // 端末上でこの関数が実行されているかを確認する if (Device.isDevice) { // 通知の権限の状態を取得する const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; // 通知が拒否されている場合 if (existingStatus !== "granted") { // アラートを表示して、通知の許可を取得する const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } // 結局通知を拒否された場合 if (finalStatus !== "granted") { alert("Failed to get push token for push notification!"); return; } token = (await Notifications.getExpoPushTokenAsync()).data; } else { alert("Must use physical device for Push Notifications"); } if (Platform.OS === "android") { Notifications.setNotificationChannelAsync("default", { name: "default", importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], lightColor: "#FF231F7C", }); } return token; }
OSがAndroidの場合は、setNotificationChannelAsync
メソッドを呼び出して、第一引数で指定したチャンネルIDにチャンネル構成を割り当てる。
こにれより、サーバー側でExpoPushMessage
型のオブジェクトを組み立てる時に、channelId
にここで指定したチャンネルIDを指定すると、ここで設定したチャンネル構成で通知が届くようになる。
トークンの取得に成功したら、任意のタイミングでサーバーに送信して保存するようにしてみる。
// screens/SetupWizard/NotificationSetupScreen.tsx ... const expoPushTokenMutation = useFunctionsCall< UpdateConfigWithExpoPushTokenRequestData, UpdateConfigWithExpoPushTokenResponseData >(functions, "updateConfigWithExpoPushToken"); ... // 通知権限の許可とExpo Push Tokenを取得する const token = await registerForPushNotifications(); // トークンをexpo_push_tokensカラムの配列に追加する if (token) { expoPushTokenMutation.mutate({ expo_push_token: token, }); } ...
通知がタップされた場合の挙動を定義するためのフックを実装する
expo-notifications
から公開されているuseLastNotificationResponse
フックを使うことで、直近でタップした通知の情報をレスポンスとして取得することができる。
この内容をuseEffect
で処理することで、アプリがフォアグラウンドだろうと、閉じていた状態から通知をタップしてアプリが起動した状態だろうと、そのタップした通知のdata
から求められている遷移先をurl
として取得することができる。
// hooks/useExpoNotifications.ts import * as Notifications from "expo-notifications"; import { useEffect } from "react"; import * as Linking from "expo-linking"; export function useExpoNotifications() { const lastNotificationResponse = Notifications.useLastNotificationResponse(); useEffect(() => { // data(JSON)の例 // { "url": "scheme://path/into/app" } if ( lastNotificationResponse && lastNotificationResponse.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER ) { if ( lastNotificationResponse.notification.request.content.data.url && typeof lastNotificationResponse.notification.request.content.data .url === "string" ) { Linking.openURL( lastNotificationResponse.notification.request.content.data.url ); } } }, [lastNotificationResponse]); return { response: lastNotificationResponse, }; }
あとはこのフックをRootNavigator
などで呼び出すと良い。すると通知がタップされれば任意のアクションを実行することができる。
ここでは特定の画面に遷移するようにしている。
// navigation/index.tsx ... const Stack = createNativeStackNavigator<RootStackParamList>(); ... function RootNavigator() { ... const { response } = useExpoNotifications(); ... return ( <Stack.Navigator> ... </Stack.Navigator> ) }
まとめ
今回の記事は前回公開したこちらの記事と一緒に読んでいただけるとより理解が深まるのではと思います。
expo-notifications
の詳細についてはこちらの公式ドキュメントを参照していただけると助かります。