20191218のNode.jsに関する記事は20件です。

TypeScript の Decorator Hell を解消する

これを解決します。

src/models/user.ts
import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class User {
  @PrimaryGeneratedColumn()
  @ApiProperty({ example: 1 })
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  @Column()
  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  @Column()
  @ApiProperty({ example: 'alice' })
  name!: string;

  @MaxLength(140)
  @Column('text')
  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

この記事は NestJS アドベントカレンダー 2019 18 日目の記事です。

はじめに

NestJS + ClassValidator + TypeORM 、という構成などのときに、上記のような Decorator Hell を想像してしまうことはあると思います。
動くものとしては十分ですが、メンテナンス性を高めるために、 Abstract Class と Interface を活用して分離し、依存関係を整理する一例を紹介します。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day18-avoid-decorator-hell

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の作成・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

現実装の Decorator の挙動については Decorator と継承 にも書いていますので併せてお読み下さい。

Validator を分離する

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  @PrimaryGeneratedColumn()
  @ApiProperty({ example: 1 })
  id!: number;

  @Column()
  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @Column()
  @ApiProperty({ example: 'alice' })
  name!: string;

  @Column('text')
  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

class-validator が継承した Class でも validation ができることを利用し、 validation の定義を親クラスに移譲します。
以下のコードを実行すると、バリデーションエラーが発生します。

import { User } from './src/models/user';
import { validate } from 'class-validator';

async function main() {
  const user = new User();
  user.id = 1;
  user.displayId = 'alice1234567890123456';
  user.name = 'alice';

  const err = await validate(user, { skipMissingProperties: true });
  console.log(err);
}

main().catch(console.error);

API 層を分離する

API レスポンスとして使用される / Swagger のドキュメント生成に使用される Class を別に定義します。

import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

type TransferUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class TransferUser extends User implements TransferUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;
}
src/app.controller.ts
import { Controller, Get, HttpException, Query } from '@nestjs/common';
import { TransferUser } from './models/user';
import { ApiResponse } from '@nestjs/swagger';
import { validate } from 'class-validator';

@Controller()
export class AppController {
  @Get()
  @ApiResponse({ status: 200, type: TransferUser })
  @ApiResponse({ status: 400 })
  async getUser(
    @Query() { displayId, name }: { displayId: string; name: string },
  ): Promise<TransferUser> {
    if (!displayId || !name) {
      throw new HttpException('displayId and name are required', 400);
    }

    const user = new TransferUser();
    user.id = 123;
    user.displayId = displayId;
    user.name = name;

    const errs = await validate(user, { skipMissingProperties: true });

    if (errs.length) {
      console.error(errs);
      throw new HttpException(errs, 400);
    }

    console.log(user);

    return user;
  }
}
$ curl localhost:3000\?displayId=alice07\&name=alice
{"id":123,"displayId":"alice07","name":"alice"}

$ curl localhost:3000\?displayId=alice1234567890123456\&name=alice
[{"target":{"id":123,"displayId":"alice1234567890123456","name":"alice"},"value":"alice1234567890123456","property":"displayId","children":[],"constraints":{"maxLength":"displayId must be shorter than or equal to 16 characters"}}]

TypeORM 層を分離する

次に、 User Class から TypeORM の Decorator を分離します。

import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  id!: number;
  displayId!: string;
  name!: string;
  profileText?: string;
  createdAt!: number;
  updatedAt!: number;
}

type SerializableUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class SerializableUser extends User implements SerializableUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;
}

export class UserEntity extends User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

ロジックを持ち基底となる Pure な User を用意し、整理する

上記の手順で User Class は class-validator を継承しているため、基底とは言えません。
なので、基底となる、 Decorator のない Pure TypeScript な User Class として定義するよう、継承関係を整理します。
また、ここで実装される toObject メソッドは User を継承した全ての Class で使用できるメソッドになります。

export class User {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;

  constructor({
    id,
    displayId,
    name,
    profileText,
    createdAt,
    updatedAt,
  }: User) {
    this.id = id;
    this.displayId = displayId;
    this.name = name;
    this.profileText = profileText;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

export class ValidatableUser extends User {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

type TransferUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class TransferUser extends ValidatableUser
  implements TransferUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
    };
  }
}

export class UserEntity extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

Abstract Class 、 Interface を活用し整理する

最後に、 インスタンス化しないものを Abstract Class 化します。
この Abstract Class も、 toObject された値も、ともに満たす Interface を定義し実装します。

export interface UserInterface {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;
}

export abstract class AbstractUser implements UserInterface {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;

  constructor({
    id,
    displayId,
    name,
    profileText,
    createdAt,
    updatedAt,
  }: UserInterface) {
    this.id = id;
    this.displayId = displayId;
    this.name = name;
    this.profileText = profileText;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  toObject(): UserInterface {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

export abstract class ValidatableUser extends AbstractUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt?: number;
  updatedAt?: number;
}

export type TransferUserType = Omit<UserInterface, 'createdAt' | 'updatedAt'>;

export class User extends ValidatableUser {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
    };
  }
}

export class UserEntity extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt?: number;

  @Column()
  updatedAt?: number;
}

この状態でも、ロジック(Controller にロジックを書くべきではないとは思いますが例なので)側からは自然に見えるように思います。

src/app.controller.ts
@Controller()
export class AppController {
  @Get()
  @ApiResponse({ status: 200, type: User })
  @ApiResponse({ status: 400 })
  async getUser(
    @Query() { displayId, name }: { displayId: string; name: string },
  ): Promise<UserInterface> {
    if (!displayId || !name) {
      throw new HttpException('displayId and name are required', 400);
    }

    const user = new User({ id: 123, displayId, name });

    const errs = await validate(user, { skipMissingProperties: true });

    if (errs.length) {
      console.error(errs);
      throw new HttpException(errs, 400);
    }

    console.log(user);

    return user.toObject();
  }
}

ここまで分離する必要があるかどうかはケースバイケースかと思いますが、 Decorator を提供する複数のライブラリに同時に依存してしまうリスクをある程度排除し、同時にメンテナンス性もある程度担保できるかと思います。

おわりに

NestJS + ClassValidator + TypeORM 、という構成などのときに、 Abstract Class と Interface を活用して Decorator Hell を解消する方法の一例を紹介しました。
この方法が全てのプロジェクトに当てはまるわけではありませんが、参考にしていただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Decorator Hell を解消する

これを解決します。

src/models/user.ts
import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class User {
  @PrimaryGeneratedColumn()
  @ApiProperty({ example: 1 })
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  @Column()
  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  @Column()
  @ApiProperty({ example: 'alice' })
  name!: string;

  @MaxLength(140)
  @Column('text')
  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

この記事は NestJS アドベントカレンダー 2019 18 日目の記事です。

はじめに

NestJS + ClassValidator + TypeORM 、という構成などのときに、上記のような Decorator Hell を想像してしまうことはあると思います。
動くものとしては十分ですが、メンテナンス性を高めるために、 Abstract Class と Interface を活用して分離し、依存関係を整理する一例を紹介します。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day18-avoid-decorator-hell

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の作成・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

現実装の Decorator の挙動については Decorator と継承 にも書いていますので併せてお読み下さい。

Validator を分離する

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  @PrimaryGeneratedColumn()
  @ApiProperty({ example: 1 })
  id!: number;

  @Column()
  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @Column()
  @ApiProperty({ example: 'alice' })
  name!: string;

  @Column('text')
  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

class-validator が継承した Class でも validation ができることを利用し、 validation の定義を親クラスに移譲します。
以下のコードを実行すると、バリデーションエラーが発生します。

import { User } from './src/models/user';
import { validate } from 'class-validator';

async function main() {
  const user = new User();
  user.id = 1;
  user.displayId = 'alice1234567890123456';
  user.name = 'alice';

  const err = await validate(user, { skipMissingProperties: true });
  console.log(err);
}

main().catch(console.error);

API 層を分離する

API レスポンスとして使用される / Swagger のドキュメント生成に使用される Class を別に定義します。

import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

type TransferUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class TransferUser extends User implements TransferUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;
}
src/app.controller.ts
import { Controller, Get, HttpException, Query } from '@nestjs/common';
import { TransferUser } from './models/user';
import { ApiResponse } from '@nestjs/swagger';
import { validate } from 'class-validator';

@Controller()
export class AppController {
  @Get()
  @ApiResponse({ status: 200, type: TransferUser })
  @ApiResponse({ status: 400 })
  async getUser(
    @Query() { displayId, name }: { displayId: string; name: string },
  ): Promise<TransferUser> {
    if (!displayId || !name) {
      throw new HttpException('displayId and name are required', 400);
    }

    const user = new TransferUser();
    user.id = 123;
    user.displayId = displayId;
    user.name = name;

    const errs = await validate(user, { skipMissingProperties: true });

    if (errs.length) {
      console.error(errs);
      throw new HttpException(errs, 400);
    }

    console.log(user);

    return user;
  }
}
$ curl localhost:3000\?displayId=alice07\&name=alice
{"id":123,"displayId":"alice07","name":"alice"}

$ curl localhost:3000\?displayId=alice1234567890123456\&name=alice
[{"target":{"id":123,"displayId":"alice1234567890123456","name":"alice"},"value":"alice1234567890123456","property":"displayId","children":[],"constraints":{"maxLength":"displayId must be shorter than or equal to 16 characters"}}]

TypeORM 層を分離する

次に、 User Class から TypeORM の Decorator を分離します。

import { IsNotEmpty, MaxLength } from 'class-validator';
import { Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

export class ValidatableUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

export class User extends ValidatableUser {
  id!: number;
  displayId!: string;
  name!: string;
  profileText?: string;
  createdAt!: number;
  updatedAt!: number;
}

type SerializableUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class SerializableUser extends User implements SerializableUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;
}

export class UserEntity extends User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

ロジックを持ち基底となる Pure な User を用意し、整理する

上記の手順で User Class は class-validator を継承しているため、基底とは言えません。
なので、基底となる、 Decorator のない Pure TypeScript な User Class として定義するよう、継承関係を整理します。
また、ここで実装される toObject メソッドは User を継承した全ての Class で使用できるメソッドになります。

export class User {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;

  constructor({
    id,
    displayId,
    name,
    profileText,
    createdAt,
    updatedAt,
  }: User) {
    this.id = id;
    this.displayId = displayId;
    this.name = name;
    this.profileText = profileText;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

export class ValidatableUser extends User {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt!: number;
  updatedAt!: number;
}

type TransferUserType = Omit<User, 'createdAt' | 'updatedAt'>;

export class TransferUser extends ValidatableUser
  implements TransferUserType {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
    };
  }
}

export class UserEntity extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt!: number;

  @Column()
  updatedAt!: number;
}

Abstract Class 、 Interface を活用し整理する

最後に、 インスタンス化しないものを Abstract Class 化します。
この Abstract Class も、 toObject された値も、ともに満たす Interface を定義し実装します。

export interface UserInterface {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;
}

export abstract class AbstractUser implements UserInterface {
  id: number;
  displayId: string;
  name: string;
  profileText?: string;
  createdAt?: number;
  updatedAt?: number;

  constructor({
    id,
    displayId,
    name,
    profileText,
    createdAt,
    updatedAt,
  }: UserInterface) {
    this.id = id;
    this.displayId = displayId;
    this.name = name;
    this.profileText = profileText;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  toObject(): UserInterface {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}

export abstract class ValidatableUser extends AbstractUser {
  id!: number;

  @IsNotEmpty()
  @MaxLength(16)
  displayId!: string;

  @IsNotEmpty()
  @MaxLength(16)
  name!: string;

  @MaxLength(140)
  profileText?: string;

  createdAt?: number;
  updatedAt?: number;
}

export type TransferUserType = Omit<UserInterface, 'createdAt' | 'updatedAt'>;

export class User extends ValidatableUser {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'alice07' })
  displayId!: string;

  @ApiProperty({ example: 'alice' })
  name!: string;

  @ApiProperty({ example: `Hello, I'm NestJS Programmer!` })
  profileText?: string;

  toObject() {
    return {
      id: this.id,
      displayId: this.displayId,
      name: this.name,
      profileText: this.profileText,
    };
  }
}

export class UserEntity extends ValidatableUser {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  displayId!: string;

  @Column()
  name!: string;

  @Column('text')
  profileText?: string;

  @Column()
  createdAt?: number;

  @Column()
  updatedAt?: number;
}

この状態でも、ロジック(Controller にロジックを書くべきではないとは思いますが例なので)側からは自然に見えるように思います。

src/app.controller.ts
@Controller()
export class AppController {
  @Get()
  @ApiResponse({ status: 200, type: User })
  @ApiResponse({ status: 400 })
  async getUser(
    @Query() { displayId, name }: { displayId: string; name: string },
  ): Promise<UserInterface> {
    if (!displayId || !name) {
      throw new HttpException('displayId and name are required', 400);
    }

    const user = new User({ id: 123, displayId, name });

    const errs = await validate(user, { skipMissingProperties: true });

    if (errs.length) {
      console.error(errs);
      throw new HttpException(errs, 400);
    }

    console.log(user);

    return user.toObject();
  }
}

ここまで分離する必要があるかどうかはケースバイケースかと思いますが、 Decorator を提供する複数のライブラリに同時に依存してしまうリスクをある程度排除し、同時にメンテナンス性もある程度担保できるかと思います。

おわりに

NestJS + ClassValidator + TypeORM 、という構成などのときに、 Abstract Class と Interface を活用して Decorator Hell を解消する方法の一例を紹介しました。
この方法が全てのプロジェクトに当てはまるわけではありませんが、参考にしていただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsのバージョンを上げた際のnode-sassのビルドエラー

ほんとにどうってこと無いメモです。

久々に開発しようとしたNuxtJSプロジェクトで、node-sassがコケました。
node-sassはv4.13.0です。

$ yarn dev

・
・
・

● Client █████████████████████████ building (61%) 431/466 modules 35 active
 node_modules/markdown-it/lib/rules_core/state_core.js

✖ Server
  Compiled with some errors in 9.34s


✖ Client
  Compiled with some errors in 13.93s

✖ Server
  Compiled with some errors in 9.34s


 ERROR  Failed to compile with 1 errors                                            friendly-errors 23:04:34


 ERROR  in ./layouts/blog.vue?vue&type=style&index=0&lang=scss&                    friendly-errors 23:04:34

Module build failed (from ./node_modules/sass-loader/dist/cjs.js):                 friendly-errors 23:04:34
Error: Missing binding /Users/n0bisuke/dotstudio/1_protooutstudio/node_modules/node-sass/vendor/darwin-x64-79/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

・
・
・

みたいなコケかたをしました。

Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 13.x

この64-bitをみると一瞬カタリナにmacOSをアップデートしたから怒られるやつかと思いましたが、

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

こちらを見ると既に64bit Node.js v12でnode-sassが紐付けされてたみたいですね。

それにしてもエラー表示がすごい。

これで解決

yarn add node-sass

npmの人も入れ直せばOK。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新入社員に適当にNode.jsのアプリ入門教えたら意外とウケたのでメモ

はじめに

細かいことは気にせずにNode.jsでwebアプリケーションを作る手順をまとめました。
新入社員の教育につかってみたら思ったより理解してくれて、開発の一歩目になってもらえたのでメモがてら置いておきます。
これから開発をする方がこの記事にたどり着いて少しでも開発への苦手意識などがなくなれば嬉しいです。

スクリーンショット 2019-12-18 22.20.07.png

SlideShareはこちら?
https://www.slideshare.net/SoheiUchino1/nodejs-beginner

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

grpc_tools から生成した gRPC クライアントを promisify してみた。

前提条件

% node --version
v12.13.0
% npm --version
6.13.2

目的

最近、grpc_toolsgrpc_tools_node_protoc_ts を併せて Typescript の型ファイルと node.js の gRPC のクライアントを生成する機会がありました。生成される GRPC クライアントなのですが、Node.js にありがちな callback にて行う非同期処理です。そのため、GraphQL のリゾルバ等と組み合わせて使用する場合、非常に使い勝手が悪いです。

今回は生成された gRPC クライアントの関数を promisify します。

gRPC クライアントの生成

例えば、次のような protocol buffer から pb ファイルを生成することを考えます。

user.proto
syntax = "proto3";

package user;

option go_package = "v1";

service UserService {
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}

message CreateUserRequest {
    int64 id = 1;
    string name = 2;
}

message CreateUserResponse {
    int64 id = 1;
    string name = 2;
}

grpc_tools_node_protoc コマンドから pb ファイルを生成します。

dist='src/grpc/generated'; \
grpc_tools_node_protoc \
  --js_out=import_style=commonjs,binary:${dist} \
  --ts_out=${dist} \
  --grpc_out=${dist} \
  -I ./proto
  ./proto/user.proto

生成された型ファイルの一部を抜粋します。

user_grpc_pb.d.ts
export class UserServiceClient extends grpc.Client implements IUserServiceClient {
    constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
    public createUser(request: user_pb.CreateUserRequest, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
    public createUser(request: user_pb.CreateUserRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
    public createUser(request: user_pb.CreateUserRequest, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: user_pb.CreateUserResponse) => void): grpc.ClientUnaryCall;
}

createUser 関数の最後の引数がそえぞれ callback となっていることがわかります。

クライアントの実装

Promise を実装する。

まずは自力で Promise を実装します。

client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());

  return new Promise((resolve, reject) => {
    client.createUser(request, (err, response) => {
      err === null ? resolve(response) : reject(err);
    })
  });
};

コレでも良いのですが、gRPC のエンドポイントが増えるたびに実装を行うのはなかなかツライです。

util.promisify を利用する。

次に Node.js の util.promisify を利用することを考えてみます。UserServiceClient クラスのメソッドを promisify しているため、bind 関数により this を束縛しないとエラーが発生することに注意してください。

client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';
import { promisify } from 'util'

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());
  return promisify<CreateUserRequest, CreateUserResponse>(client.createUser).bind(client)(request)
};

エラーの有無に起因する分岐処理はなくなりましたが、ジェネリクスを指定する必要があります。

Bluebird.js を利用する。

Promise の実装である Bluebird.js を使用すると次のようになります。

client.ts
import { UserServiceClient } from './generated/user_grpc_pb';
import { credentials } from 'grpc';
import { CreateUserRequest, CreateUserResponse } from './generated/user_pb';
import { promisify } from 'bluebird'

export const createClient = (url: string) => (
  request: CreateUserRequest
): Promise<CreateUserResponse> => {
  const client = new UserServiceClient(url, credentials.createInsecure());
  return promisify(client.createUser, { context: client })(request)
};

ジェネリクスがなくなり、this の束縛も含めてシンプルになった印象です。promisify により生成される関数の戻り値の型が Bluebird<unknown> から Promise<CreateUserResponse> へ暗黙的にキャストが行われている点が少し気になりますが...

所感

今の所、Bluebird.js を利用した promisify がベターだと感じています。そもそものクライアントの実装をなんとかしてほしいところではあります。他の良い方法があれば是非とも教えて頂きたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Lambda + API GatewayでGithub上のアクションを検知してGithubに対してアクションする

はじめに

最近バックエンドの実装をメインに担当しているエンジニアです。
先日チームメンバー(@sen-higaさん)と共同で行った業務効率化タスクを通して、初めてWebhookやサーバレスアーキテクチャに触れたので、その時の備忘録です。

やったこと

Github上のアクションを検知してGithubに対してアクションするという仕組みをLambda + API Gatewayで実装しました。
GithubからGithubへの動線がわかりやすいように、タスクの実装時とは内容を変えて、イシューがOpenされた時に、作成者をイシューに自動アサインするというシンプルな仕組みにしました。

スクリーンショット 2019-12-19 8.56.08.png

もっとこうした方がいい、自分ならこう実装するというご意見があればぜひお願いします。

開発環境

os: mac High Sierra 10.13.6
npm: 6.11.3
Node: 12.12.0

利用技術

今回、利用したのはGithub Webhook Github API,AWS Lambda, Amazon API Gatewayになります。

Github webhook

Webhookとはあるサービスでのイベント発生時に、指定したURLにPOSTリクエストする仕組みです。(webhookとは?より)
Github webhookは、Githubが提供しているwebhookで、特定のアクションがリポジトリあるいは Organization で生じたときに外部の Web サーバーへ通知を配信する方法を提供しています。(公式ドキュメントより)

例えば、イシューのwebhookを登録しておくと、イシューがopenした時に登録したエンドポイントに以下のようなリクエストが送られます。
スクリーンショット 2019-12-18 16.59.54.png

(公式ドキュメントより)

Github API

Webhookとは逆にGithubにアクションする時に使います。APIを利用するには、アクションを実行させるアカウントでアクセストークンを発行する必要があります。

AWS Lambda

サーバーレスでコードを実行できるサービスです。管理画面で、コーディング、環境変数の管理、テストなど色々できます。

Amazon API Gateway

開発規模に応じたエンドポイントを簡単に用意できるサービスです。webhookがリクエストを投げるエンドポイントを作成するために利用しました。Lambdaと連携させると、ヘッダー、ボディをJSON形式でlambdaに渡してくれます。

実装の流れ(概要)

以下のような流れで実装しました。

  1. Lambda関数の作成
  2. エンドポイントの作成
  3. Github Webhookの追加
  4. Github API用のアクセストークンの取得
  5. Lambda関数の実装
    1. リクエストのバリデーション
    2. Webhookのリクエストを解釈
    3. APIを叩けるようにモジュールを追加
    4. Github APIにリクエストを投げる
  6. 動作確認

実装の流れ(詳細)

1. Lambda関数の作成

コンソールから言語をnode.jsに指定して作成しました。

2. エンドポイントの作成

Designerの「トリガーの追加」からエンドポイントを追加しました。
「トリガーの追加」 -> 「API Gateway」を選択し、新規APIの作成画面を表示します。
今回はシンプルな動作のみ実装するのでテンプレートは「HTTP API」を選択しました。

作成後リダイレクトされたLambdaの画面下部に、デプロイ済みのエンドポイントが表示されます。こちらのエンドポイントをGithub Webhookに登録します。

3. Github Webhookの追加

3.1 Webhookに登録するトークンを生成

先程作成したエンドポイントは任意のリクエストを受け付ける状態になっているので、Webhookの認証用ヘッダーを見てリクエストを制限します。認証用ヘッダーをWebhookに埋め込んでもらうために、トークンを事前に用意しておきます。

今回は公式ドキュメント に従って生成したトークンを利用しました。

こちらのトークンは実装時にも利用するので、Lambda関数の環境変数に任意のキー名(SECRET TOKEN等)で追加しておきます。

3.2 Webhookの追加

先程作成したエンドポイントを使ってwebhookを追加します。
アクションを検知したいリポジトリに行き、 Settings -> Webhooks -> Add Webhook
から追加します。この時、先程生成したトークンをSecret欄に記入します。

また、今回はイシューのみを対象としたいので
「Let me select individual events」 -> 「issues」 にチェックを入れました。

スクリーンショット 2019-12-19 7.42.57.png

4. Github API用のアクセストークンの取得

4.1 Tokenの取得

Github API用のトークンも取得しておきます。
自分のアイコンマークを押すと表示されるメニュー -> Setting -> Developer settings -> Personal access tokens -> Generate New Token から追加します。

今回は「repo」の権限を与えたトークンを発行しました。
スクリーンショット 2019-12-17 19.20.35.png

発行されたTokenはLambdaの「環境変数」に任意のキー名(ACCSESS_TOKEN等)で登録しておきます。

これで、Githubからのアクションを検知し、Github APIを叩く動線が整いました。

5. Lambda関数の実装

以下のような手順で実装しました。

  1. リクエストのバリデーション
  2. 対象のアクションか判定
  3. APIリクエスト用にモジュールを追加
  4. GithubAPIにリクエストを投げる

5.1 リクエストのバリデーション

リクエストのバリデーションには、X-Hub-Signatureヘッダー、リクエストボディ、環境変数に登録したSECRET_TOKENを利用します。

公式ドキュメントによると、X-Hub-Signatureは、鍵をSecret Token、データをボディ として算出した値(MAC値)と一致します。

バリデーション実装部分のコードはこちらです。

exports.handler = (event) => {
    const headers = event.headers;
    const body = event.body;
    if (! isValid(body, headers)) {
        const response = {
            statusCode: 500,
            body: 'Given signatue is invalid',
        };
        return response;
    }
    const response = {
        statusCode: 200,
        body: 'OK',
    };
    return response;
};

function isValid (body, headers) {
    const crypto = require('crypto');
    const hmac = crypto.createHmac('sha1', process.env.SECRET_TOKEN);
    hmac.update(body, 'utf8');
    const signature = 'sha1=' + hmac.digest('hex');
    return signature === headers['X-Hub-Signature'];
}

2. 対象のアクションか判定する

次に、リクエストが対象のアクションのものか判定する部分を実装します。現状だとissueに関わるあらゆるアクション(edited, deleted, transferred等)に反応してしまうので、今回はopenedのみ反応するようにします。

exports.handler = (event) => {
    ...
    if (! isOpened(JSON.parse(body))) {
        const response = {
            statusCode: 400,
            body: 'Given action is invalid',
        };
        return response;
    }
    ...
};

function isValid (body, headers) {
    ...
}

function isOpened (body) {
    return body.action === 'opened';
}

これで、Webhookのリクエストを解釈する部分の実装は完了です。

3. APIリクエスト用にモジュールを追加する

続いて、APIを叩く部分の実装に入る前に、リクエストを行えるようrequestモジュールを追加しておきます。
公式の注記に従って、Layerという機能を使ってmoduleを追加しました。

追加手順は、
1.ローカルPCでrequestモジュールをnpm install
2.node_modulesのzipファイルを用意(zipファイルのディレクトリは「nodejs」が先頭です

 {ファイル名}.zip
 └ nodejs/node_modules/...

3.lambda画面左側の「Layer」からレイヤーを追加します
4.追加したレイヤーをlambda画面「Designer」の「Layers」から今回のlambdaと紐づけます
Layerで紐付けたモジュールはlambda関数から自由に利用できます。

4. GithubAPIにリクエストを投げる

APIにリクエストを投げる部分を実装します。
イシュー作成者をイシューにアサインする場合、ヘッダー、メソッド、URI、ボディは以下の
ように設定します。

ヘッダー

環境変数に追加したトークンを利用してAuthenticationヘッダーを、トークンを発行したユーザ名をUser-Agentヘッダーに追加します

URI

イシューを更新する場合は
{ルートエンドポイント}/repos/:owner/:repo/issues/:issue_numberを指定します

 メソッド

イシューを更新する場合はPATCHを指定します

リクエストボディ

JSON形式でパラメータを記入します。(
今回はAssigneeを指定するパラメータ)

exports.handler = (event) => {
    ...
    const request = require('request');
    request(params(JSON.parse(body)), (error, response, body) => {
        if (error) {
            console.error('Issue assign failed');
        } else {
            console.log('Issue assign success');
        }
    });
    ...
};

...

function params (body) {
    return {
        json: true,
        headers: {
            'Authorization': 'token ' + process.env.ACCESS_TOKEN,
            'User-Agent': 'yanagimura'
        },
        method: 'PATCH',
        uri: `${body.issue.repository_url}/issues/${body.issue.number}`,,
        json: {
            'assignee': 'yanagimura'
        }
    };
}

6. 動作確認

早速イシューを作成してみます。

イシューを立てた直後にはAssineesは空ですが
スクリーンショット 2019-12-18 17.37.02.png

?

スクリーンショット 2019-12-18 17.37.09.png

すぐにイシューを立てたユーザ(私)がアサインされました!

まとめ

 
チームメンバーと共同で実装した時から時間が経っていたため、忘れている部分も多々あり、今回記事を投稿することで復習できてよかったです。ひとつひとつの実装はシンプルでしたが、それを組み合わせて、ひとつの流れを作ろうとすると、サーバレスとはいえ結構複雑だと感じました。 

また、ヘッダーの署名やAPIリクエストの構造などの知見は、外部サービスの利用と構築の両方の観点から得るものが多かったです。

最後に

全体のコードを記載しておきます。

exports.handler = (event) => {
    const headers = event.headers;
    const body = event.body;
    if (! isValid(body, headers)) {
        const response = {
            statusCode: 500,
            body: 'Given signatue is invalid',
        };
    }
    if (! isOpened(JSON.parse(body))) {
        const response = {
            statusCode: 400,
            body: 'Given action is invalid',
        };
        return response;
    }
    const request = require('request');
    request(params(JSON.parse(body)), (error, response, body) => {
        if (error) {
            console.error('Issue assign failed');
            console.error(error);
        } else {
            console.log('Issue assign success');
            console.log(response);
            console.log(body);
        }
    });
    const response = {
        statusCode: 200,
        body: 'OK',
    };
    return response;
};

function isValid (body, headers) {
    const crypto = require('crypto');
    const hmac = crypto.createHmac('sha1', process.env.SECRET_TOKEN);
    hmac.update(body, 'utf8');
    const signature = 'sha1=' + hmac.digest('hex');
    return signature === headers['X-Hub-Signature'];
}

function isOpened (body) {
    return body.action === 'opened';
}

function params (body) {
    return {
        json: true,
        headers: {
            'Authorization': 'token ' + process.env.ACCESS_TOKEN,
            'User-Agent': 'yanagimura'
        },
        method: 'PATCH',
        uri: `${body.issue.repository_url}/issues/${body.issue.number}`,
        json: {
            'assignee': `${body.sender.login}`
        }
    };
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゲームにおけるFirebase 活用例

PONOS Advent Calendar 2019の24日目の記事です。
?メリークリスマス!!クリスマス・イブにFirebaseの記事をお届けします!!!?
???????????????????????????

はじめに:eyeglasses:

現在運用中のゲームでFirebaseを導入しました。
この記事では活用事例とノウハウを紹介していきます。
Firebase利用の一例になれば幸いです。

組み込み方法は別記事をご参照ください
公式ドキュメント
iOS
Android
Unity
C++
Web

この記事の対象者:eyeglasses:

・Firebase初心者、中級者
・Firebaseを検討中の方
・Firebaseの実装イメージを掴みたい方
・サーバーレスを検討中の方
・Googleのサービスが好きな方、興味のある方

Firebaseの各機能をざっくり紹介

機能がたくさんあります

名称 機能 活用
Firebase Authentication 認証機能
Firebase Realtime Database NoSQL クラウド データベースでデータの保管と同期を行うことができる
Cloud Firestore Realtime Database からさらにパワーアップした NoSQL データベース。
Cloud Storage for Firebase 写真や動画などのコンテンツを保管、取得することができる。
Firebase Hosting webサイトを構築
Cloud Functions for Firebase 関数ごとにサーバー処理を簡単に用意できる
ML Kit for Firebase 機械学習 ×
Firebase Crashlytics クラッシュ レポート管理
Firebase Performance Monitoring パフォーマンスの問題を診断
Firebase Test Lab 様々なデバイスでアプリの自動テストおよびカスタマイズされたテストを実行
Firebase Cloud Messaging サーバから通知機能
Firebase In-App Messaging ターゲットを絞り込んでアプリ内で通知 ×
Firebase Predictions 機械学習を用いた分析ツール
Firebase Remote Config Firebase consoleから、外観の変更、機能の段階的な展開などをカスタマイズ ×
Google Analytics for Firebase 総合的な分析ツール
Firebase A/B Testing ユーザーごとに出し分けしてA/Bテストを行う
Firebase Dynamic Links 複数のプラットフォームで機能するURLを生成 ×
Firebase App Indexing アプリを Google 検索結果に表示することができる ×

活用1:ログイン時のユーザー認証

Firebase Authenticationをゲーム内での認証機能として使っています。
ログイン時に認証して、Firebase各種機能と連携して認証済みのアカウントしかセキュリティー的に許可しないようにしています。
以下の4種類の認証を使っています。
・Google
・Facebook
・メールアドレス
・匿名認証

匿名認証で作成したデータと各種連携したデータを紐づける処理はこちらで実装する必要がありますが、
公式ドキュメントにもサンプルコードが用意されているので低コストで実装できます。

匿名認証のコード

Auth.cpp
#include "firebase/app.h"
#include "firebase/auth.h"

void Auth::signInAnonymously(){
    firebase::App* app = App::GetInstance();
    firebase::auth::Auth* auth = Auth::GetAuth(app);

    auth->SignInAnonymously().OnCompletion([this](const firebase::Future<firebase::auth::User*>& result){
        if (result.status() == firebase::kFutureStatusComplete &&
         result.error() == firebase::auth::kAuthErrorNone &&
         result.result()) {
            //成功処理
        }else{
           //失敗処理
        }
    });
}


活用2:通信時の不正対策

通信時の不正対策として
Firebase Authentication

Cloud Functions
を組み合わせてサーバーサイドでも認証チェックしております。

Cloud Functionsを使用するとクライアント側の通信処理もセキュアでシンプルな作りになります。
詳細はCloud Functionsのドキュメントを一読するのもありです。

クライアント側の通信処理
Cloud Functionsで定義したsampleにリクエストを投げます。

Request.cpp
void Request::callSample(){
    firebase::Variant data = firebase::Variant::EmptyMap();

    firebase::functions::HttpsCallableReference doSomething = functions->GetHttpsCallable("sample");
    doSomething.Call(data).OnCompletion([this]
    (firebase::Future<firebase::functions::HttpsCallableResult> result){
        if(result.status() == firebase::kFutureStatusComplete &&
        result.error() == firebase::functions::kErrorNone && result.result()){
        //成功処理
        }else{
        //失敗処理
        }
    });
}


サーバー側の認証チェック
exports.sampleの部分がレスポンスを返します。

Server.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
 
exports.sample = functions.https.onCall((data, context) => {
    if(!checkAuth (data, context) ){
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +'while authenticated.');
    }
    //以降でサーバー処理を記入していく 
    return {
            text:"success",
    };
});


//認証チェック
async function checkAuth (data, context) {
    if( !(context.auth && context.auth.uid && context.auth.token)){
        console.log('checkAuth error');
        return false;
    }
    const user = await admin.auth().getUser(context.auth.uid);
    if (user.customClaims && user.customClaims.admin !== true) {
        console.log('checkAuth error');
        return false;
    }
    return true;
}

活用3:サーバーからのPush通知

Cloud Messagingを活用して特定条件を満たしたユーザーにPush通知を送るようにしています。
このサンプルソースではRealTimeDBにユーザーのpushTokenが保存されており、pushTokenを取り出して
使用しているパターンです。

Server.js
exports.pushNotification = functions.https.onCall((data, context) => {
    if(!checkAuth (data, context) ){
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
        'while authenticated.');
    }


    const userId = data.userId;
    var pass = "user_table/" + userId + "/" + "pushToken";
    return admin.database().ref(pass).once('value').then(function(snapshot){
        var pushToken = snapshot.val();
        const payload = {
            notification: {
              title: 'プッシュ通知タイトル',
              body: `プッシュ通知本文`
            }
          };
          return admin.messaging().sendToDevice(pushToken, payload);
    }).catch(error => {
          console.error(error);
          throw new functions.https.HttpsError(error);
    });
});

シンプルに書くとこれだけで通知を送れます。

          return admin.messaging().sendToDevice(pushToken, payload);

活用4:バトルログの保存(アップロード)

バトル時のログや詳細情報はCloud Storageに保存しています。
DBに保存すると量と値段の面で、デメリットがあったので分析の必要がなく、他のユーザーから参照されないデータはCloud StorageにGZip化して保存しています。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"

void CloudStorage::storageUpLoad(){
    firebase::App* app = App::GetInstance();
    firebase::storage::Storage* storage = Storage::GetInstance(app);

    //アップロード先のパス
    std::string serverPath = "/";
    //アップロードするファイル保存先
    std::string localPath = "file://" + "ローカルパス";

    storage->GetReference().Child(serverPath).PutFile(localPath.c_str()).OnCompletion([](const firebase::FutureBase& result) {
                if (result.error() == firebase::storage::kErrorNone){
                  //成功処理
                }
                else {
                  //失敗処理
                }
    });

}


活用5:リソースダウンロード

これもCloud Storageを活用しています。
リソース数、容量などによって状況は変わると思いますが、問題ない速度でダウンロードできています。
何よりもCloud Storageはコストパフォーマンスがいいです。
現在、ほぼ無料枠で済んでいます。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"

void CloudStorage::storageDownload(){
    firebase::App* app = App::GetInstance();
    firebase::storage::Storage* storage = Storage::GetInstance(app);

    //対象リソースのパス
    std::string serverPath = "/";
    //ダウンロード後の保存先
    std::string localPath = "file://" + "ローカルパス";
    storage->GetReference().Child(serverPath).GetFile(localPath.c_str()).OnCompletion([](const firebase::FutureBase& result) {
        if (result.error() == firebase::storage::kErrorNone) {
         //成功処理
        }
        else {
         //失敗処理
        }
    });
}


活用6:ゲーム内お知らせ

お知らせ機能はFirebase Hostingを活用しています。
環境構築などの導入は非常に簡単です。
5分くらいでできると思います。
環境構築

環境構築が終わったら、HTML,CSS,画像などのWEB素材を用意して

firebase deploy

で、デプロイして完了です。

固定のお知らせやヘルプページを見せるだけでしたらFirebase Hostingだけで十分なのですが
ユーザーごとに見せるページを変えたい、期間で出し分けをしたい
などの仕様によってはサーバー側でHTMLを生成する必要があるので
うちのゲームではCloud Function、RealTimeDatabaseなども絡めて使っている箇所があります。

注意点としては
過去にデプロイしたWEBページがCDNに溜まっていき、容量が無駄になるので
Firebaseコンソール上からときどき消去しないといけません。
WEBページに問題があったときに、とっさにロールバックできるのでそれとトレードオフという感じですね

活用7:KPI 分析

ゲーム運営にKPI 分析は必須ですよね
Google Analytics for Firebaseを使っています。
for Firebaseと書いていますが、バックエンドの仕組みはGoogle AnalyticsでKPI分析するうえでは違いはありません。違いは組み込み方法が若干違うくらいです。

スクリーンショット 2019-12-19 18.55.43.png

こちらのFirebase デモプロジェクトを触ってみるのが理解が早いです。

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらAnalyticsページへ
2.デモプロジェクト Analyticsページ
※リンクから飛べない場合は、左側のメニューにてアナリティクス配下のDashboardをクリック

Analyticsは組み込むだけでセッション数、DAU、課金額、ARPPUなど
デフォルトでいくつかの値を集計してくれます。
ゲーム内独自の集計をしたい場合は、イベントを実装すればできます。

イベント実装例

FirebaseAnalytics.cpp
#include "firebase/app.h"
#include "firebase/analytics.h"

void FirebaseAnalytics::init(){
    firebase::App* app = App::GetInstance();
    firebase::analytics::Initialize(*app);
}

//レベルアップ時、イベント登録
void FirebaseAnalytics::levelUp(){
    firebase::analytics::LogEvent(firebase::analytics::kEventLevelUp);
}

活用8:予測データの運営活用

Firebase PredictionsではGoogleの機械学習の予測結果を簡単に知ることができます。

スクリーンショット 2019-12-19 19.03.49.png

デモプロジェクト

以下項目の一週間後の予測結果を知ることができます。
churn離脱ユーザーの数

not_churn継続ユーザーの数

spend課金するユーザーの数

not_spend課金しないユーザーの数

Firebase Predictionsの優れている点は
これらの予測結果に対して
・A/Bテストを行なって動向を分析できる
・Remote ConfigによってUIやパラメーターなどを瞬時に変えることができる
・Cloud Messagingを使ってお得な情報などを送ることができる
ことにあると思います。

また、Google Analyticsと連携してコンバージョンに設定されたイベントを予測項目として追加することもできます。
ゲームごとに有効なイベントを設定すれば有効活用できると思います。

活用9:バナー広告、動画広告

Google AdMobを使って、バナー広告、動画広告に対応することができます。
Firebaseの機能としてあまり注目されてないですが、実はFirebase SDKではAdMobの機能もサポートされてます。

バナー広告表示

FirebaseAdmob.cpp
#include "firebase/app.h"
#include "firebase/admob.h"
#include "firebase/admob/banner_view.h"


void FirebaseAdmob::createBanner(){
    //firebase::admob::BannerView* banner_view
    banner_view = new firebase::admob::BannerView();

    firebase::admob::AdSize ad_size;
    ad_size.ad_size_type = firebase::admob::kAdSizeStandard;
    ad_size.width = 320;
    ad_size.height = 50;
    // my_ad_parent is a reference to an iOS UIView or an Android Activity.
    // This is the parent UIView or Activity of the banner view.
    // kBannerAdUnitはAdMobのサイトで発行されるId
    banner_view->Initialize(getAdParent(), kBannerAdUnit, ad_size);
}

void FirebaseAdmob::showBanner(){
    banner_view->MoveTo(banner_view->kPositionBottomLeft);
    banner_view->Show();
    firebase::admob::AdRequest my_ad_request = {};
    banner_view->LoadAd(my_ad_request);
}

活用10:多端末テスト

スマホゲーム開発において多端末検証って大変ですよね
特にAndroidとかAndroidとかAndroidとか・・・
そこで活躍するのが
Firebase Test Lab
Google データセンターでホストされているデバイス上でアプリをテストします。
1 回のオペレーションで、さまざまなデバイス、
さまざまなデバイス構成で Android アプリや iOS アプリをテストし、
Firebase consoleで結果(ログ、動画、スクリーンショットなど)
を確認できます。

・Android端末が足りない場合
・テスターが足りない場合
・QA開始前のシステム的なテストをしたい場合
など使いどころはたくさんあります。

スクリーンショット 2019-12-19 19.04.49.png

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらTest Labページへ
2.デモプロジェクト Test Labページ
※リンクから飛べない場合は、左側のメニューにて品質、配下のTest Labをクリック

おわりに

Firebaseを導入してみての感想としては・・・

非常に簡単、便利で素晴らしいです!
ゲーム仕様や料金面での検討をする必要はありますが
まずは無料プランで使ってみてから他のサービスと比較検討する方法がオススメです!

料金面にて実装で創意工夫した箇所もありますが、結果として今は超・低コストで運営できています。

以下の機能は無料で使い続けることができるので、組み込んでおいて損はないです
・Google Analytics for Firebase
・Firebase Authentication
・Firebase Crashlytics
・Firebase Performance Monitoring
・Firebase Cloud Messaging
・Firebase Predictions
・Firebase Dynamic Links
・Firebase App Indexing
・Firebase A/B Testing

本プロジェクトではGCPも併用して活用しています。
次回はGCPのゲーム活用事例を書きたい思います。

明日は@honeniqさんの記事です
最終日の記事も楽しみですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue + Expressのテンプレート作成メモ

はじめに

バックエンドをNode.js、フロントエンドをVue.jsでwebアプリを開発することが増えたので簡単にひな形を作る手順を残しておこうと思います。

手順

expressのプロジェクト作成

express プロジェクト名

vueのプロジェクトを作成

expressで作ったプロジェクトのルートディレクトリへ移動し
vue create public
publicというタイトルになっているので、気になる方はvueプロジェクトのindex.htmlのtittleタグを編集

vueプロジェクトでビルドする

publicディレクトリへ移動し
npm run build

expressのapp.jsに記述されているpublicのパスを変える

app.use(express.static(path.join(__dirname, 'public’)));

app.use(express.static(path.join(__dirname, 'public/dist’)));

一応この記述も消しておく

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade'); 

node.jsプロジェクトでサーバー起動

npm start

アクセスするとVueの初期画面が表示される

localhost:3000

おわりに

さくっと雛形を作ってすぐに開発に取り掛かりましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js boilerplate / Authentication from scratch - (express, graphql, mongodb)

1*htOb7MGk4cXDpp4md_iHSQ.png

A boilerplate for Node.js apps / API server / Authentication from scratch - express, graphql - (graphql compose), mongodb - (mongoose).

https://github.com/watscho/express-graphql-mongodb-boilerplate :snowman2:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm-audit-actionをマーケットプレイスに公開してみた

はじめに

CI/CD Advent Calendar 2019 1日目に毎日npm auditを実行して脆弱性対応する取り組みを紹介しました。このときはCircleCI上で実行していたのですが、GitHub Actionsを使ってみたい、自作Actionを開発してみたいと思ってやってみました。

GitHub Actions Advent Calendar 2019 16日目のphp-audit-action(β版)をマーケットプレイスに公開してみたを読んだときは、出だしからnpm auditと書いてあるし、ネタが丸かぶりかとヒヤヒヤしました。記事タイトルを拝借しました。

作ったもの

npm auditを実行するActionを作りました。

リポジトリ、マーケットプレイス

https://github.com/oke-py/npm-audit-action
https://github.com/marketplace/actions/npm-audit-action

仕様

v1.1.0時点では大きく2つの機能があります。いずれもnpm auditを実行して脆弱性があればレポートする点は同じですが、トリガーによって動作が異なります。

Pull Requestがトリガーの場合

脆弱性があれば該当のPRに対してコメントをつけます。また、ジョブ自体は失敗とします。

demo.png

Pull Request以外がトリガーの場合

Pushやスケジュール実行時に脆弱性があればIssueを作成します。

demo2.png

使い方

こんな感じです。

.github/workflows/audit.yml
name: npm audit

on:
  pull_request:
  push:
    branches:
      - master
      - 'releases/*'
# on:
#   schedule:
#     - cron: '0 10 * * *'

jobs:
  scan:
    name: npm audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: install dependencies
        run: npm ci
      - uses: oke-py/npm-audit-action@v1.1.0
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          issue_assignees: oke-py
          issue_labels: vulnerability,test

作り方

テンプレートの利用

6日目1に紹介されていた公式のTypeScriptテンプレート2を利用しました。

npm installのエラー(うろ覚え)

何か宣言が足りないとかでエラーになった気がします。以下のように自分で定義するとエラーが解消されました。

@types/octokit/index.d.ts
declare module '@octokit/graphql' {
  export type Variables = any
  export type GraphQlQueryResponse = any
}

GitHub contextの取得

トリガーに応じて動作を変えるため、githubコンテキスト3を利用しました。ユーザーが指定しなくて済むようにaction.ymlでデフォルト値として指定しました。

action.yml
...
inputs:
  github_context:
    description: 'The `github` context'
    default: ${{ toJson(github) }}
    required: false
...

ローカルでの動作確認

開発中はローカルでActionを実行して動作確認したかったのですが、inputsの渡し方がわかりませんでした。そこでソースコード4を読んでみたところ環境変数として指定できることがわかりました。

packages/core/src/core.ts
export function getInput(name: string, options?: InputOptions): string {
  const val: string =
    process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''
  if (options && options.required && !val) {
    throw new Error(`Input required and not supplied: ${name}`)
  }

  return val.trim()
}
INPUT_ISSUE_ASSIGNEES=oke-py INPUT_ISSUE_LABELS=vulnerability,test node lib/main.js

PRコメント

GitHubの操作は@octokit/rest5を利用しましたが、単純なコメントをつけることができませんでした6。特定のコミット、行に対するコメントのみつけることができました。

To add a regular comment to a pull request timeline, see "Comments."

そこで、axiosを使ってGitHub API7を直接たたくようにしました。

テストコード

公式テンプレートからリポジトリを作成するとJestを使うようになっていました。モックの使い方がよくわからず悪戦苦闘しました。一応理解した気はしますが、あまり説明できません。

テストカバレッジの取得

19日目8に投稿したのでご参照ください。

pre-commit hookの設定

ソースコードを修正して動作確認したところで、意図した通りに動かないことが多々ありました。主な原因はTypeScriptのトランスパイル忘れでした。pre-commit hookを設定することで回避しました。

.git/hooks/pre-commit
#!/bin/sh
npm run all

マーケットプレイスのアイコン設定

action.ymlのbrandingでカラーとFeatherアイコンを指定できました9。絵心ないので助かりました。

action.yml
...
branding:
  icon: 'search'
  color: 'orange'

おわりに

CI/CD Advent Calendar 2019 初日にnpm auditネタではじまり、GitHub Actions Advent Calendar 2019 最終日にnpm auditネタで終えることができました。

とりあえず自分が使いたいものはできたかなと思いますが、少しでもユーザーが増えてくれたら嬉しいです。マーケットプレイスに公開して流入はあるのでしょうか・・・? また、GitHubでスターをもらえたら嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmのパッケージグローバルインストールは憲法違反です。

結論

npxコマンドを使おう

グローバルインストール

とは

$ npm install -g elm

このように-gをつけてグローバル環境にパッケージをインストールをすることです

ローカルインストール

対して、ローカルインストールは、

$ mkdir my_project && cd my_project
$ npm init
$ npm install elm --save // or --save-dev

こんな感じでインストールすると、my_project/node_modules/の中にパッケージがインストールされます。

違い

ローカルインストールの利点としては、プロジェクト毎にpackage.jsonで管理をするため、作ったプロジェクトを本番環境や他の人の環境に渡すことが簡単になります!

そして、いろいろなプロジェクトに手を出す際に、バージョン管理が簡単になります!

そしてグローバルインストールと違い、パソコンの環境を汚染しないため気持ち良いです!

CLI系のパッケージはどうするの

グローバルインストールをした場合のCLIパッケージの実行ファイルは皆さんがnode.jsをインストールした際にパスを通したディレクトリになります。

そして、ローカルインストールをした場合の実行ファイルのインストール先はmy_project/node_modules/.bin/ディレクトリになり、プロジェクトの度にパスを通すわけにもいかないし、コマンドを打つ度に./my_project/node_modules/.bin/elmなんて打っていたら面倒臭すぎて死にたくなっちゃいますよね

このような理由がある為、CLI系のパッケージをインストールして利用する際、みんながパスを通すであろうグローバルのnode_modulesディレクトリに実行可能ファイルが作られた方が簡単に取り扱える為、Qiita等いろいろな技術サイトで

CLIなのでグローバルイントールしましょう!

などと書かれていたりします。

しかし上で書いた通り、グローバルインストールは憲法違反です

npxコマンド

ローカルにインストールした実行ファイルの参照解決をしてくれるコマンドがnpmには備わっているのです!(npmのバージョンが 5.2.0以上)

まずelmのcliをインストールしたプロジェクトを作りましょう。

$ mkdir my_project && cd my_project
$ npm init
$ npm install elm --save // or --save-dev

上記のようにインストールしたelmの実行ファイルはここにいるのでこんな感じで使えます。

$ ./node_modules/.bin/elm

でもnpxならこう!!!

$ npx elm 

さあ、グローバル環境を汚さずにelmを書こう。

まとめ

こうやって書かれているnpmのcli系パッケージの記事は

$ npm install -g {package_name}
$ {command}

全部こうやって読み替えよう!!

$ npm install {package_name} // オプションで --save or --save-dev
$ npx {command}

理由があってグローバルインストールをしている場合もあるので憲法違反じゃないかもしれないです、そのときは臨機応変に☺️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Decorator と継承

この記事は NestJS アドベントカレンダー 2019 14 日目の記事です。
寝込んでいたため遅くなり申し訳ありません。

はじめに

この記事では NestJS で多用される Decorator を継承した場合の挙動について説明します。
サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day14-decorator-and-inheritance

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の策定・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

結論

メソッドの Decorator 情報は継承されます。オーバーライドで切ることができます。

プロパティの Decorator は Class の定義時にしか評価されません。
しかし評価時にクラス名をキーにして container に副作用を与え、 instanceof で比較を行うようなライブラリでは、 instanceof は子 Class に対して親 Class と比較しても true となる(後述します)ため、継承しているような挙動に見えることがあります。

詳しくは以下で、 Method Decorator と Property Decorator に分けて説明します。

Method Decorator の挙動を追う

Decorator を定義した Class を継承した、 Decorator を直接定義していない Class のインスタンスを生成し、 Validator を定義した sayHello() を呼びます。
以下で定義する @LogProxy() は、関数の実行前後にログを出力する簡単な Decorator 関数です。

src/main.ts
function LogProxy(when: 'before' | 'after' | 'all') {
  return function(_target: any, key: string, desc: PropertyDescriptor) {
    const prev = desc.value;
    const next = function() {
      if (when === 'before' || when === 'all') {
        console.log(`${this.name}.${key} will start.`);
      }
      const result = prev.apply(this);
      if (when === 'after' || when === 'all') {
        console.log(`${this.name}.${key} has finished.`);
      }
      return result;
    };
    desc.value = next;
  };
}

class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  @LogProxy('all')
  sayHello() {
    console.log(`Hello, I am ${this.name}.`);
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}

const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
有栖.sayHello will start.
Hello, I am 有栖.
有栖.sayHello has finished.

コンパイルされた Decorator がどのような挙動をしているのか確認するため、コンパイルされたファイルを読みます。なお、 target は es2019 ですが、 2015 以降であれば Decorator 周りはほぼ変わらないようです。

dist/main.js
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function LogProxy(when) {
    return function (_target, key, desc) {
        const prev = desc.value;
        const next = function () {
            if (when === 'before' || when === 'all') {
                console.log(`${this.name}.${key} will start.`);
            }
            const result = prev.apply(this);
            if (when === 'after' || when === 'all') {
                console.log(`${this.name}.${key} has finished.`);
            }
            return result;
        };
        desc.value = next;
    };
}

class User {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`Hello, I am ${this.name}.`);
    }
}
__decorate([
    LogProxy('all')
], User.prototype, "sayHello", null);
class JapaneseUser extends User {
    constructor(name) {
        super(name);
        this.name = name;
    }
}
const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
//# sourceMappingURL=main.js.map

全てを読まずとも、 __decorate が User.prototype の name に、 decorator 関数を食わせた値を再代入していることが分かります。
下の継承している側の Class では特に defineProperty をしているわけではないので、 Decorator の影響を受け続けています。

そのため、継承した Class でオーバーライドした場合には Decorator の影響は受けません。

src/main.ts
function LogProxy(when: 'before' | 'after' | 'all') {
  return function(_target: any, key: string, desc: PropertyDescriptor) {
    const prev = desc.value;
    const next = function() {
      if (when === 'before' || when === 'all') {
        console.log(`${this.name}.${key} will start.`);
      }
      const result = prev.apply(this);
      if (when === 'after' || when === 'all') {
        console.log(`${this.name}.${key} has finished.`);
      }
      return result;
    };
    desc.value = next;
  };
}

class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  @LogProxy('all')
  sayHello() {
    console.log(`Hello, I am ${this.name}.`);
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }

  sayHello() {
    console.log(`こんにちは、私は${this.name}です。`);
  }

}

const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
こんにちは、私は有栖です。

Property Decorator の挙動を追う

同様に、 Decorator を定義した Class とその子 Class を定義します。
以下で定義する @Effect() は、呼び出し時に呼び出し元とプロパティ名、引数を Container に記録する副作用を持つ Decorator 関数です。

src/main.ts
let effectContainer = {};
let effectCounter = 0;

function Effect(str: string) {
  return function(target: any, key: string) {
    const className = target.constructor.name;
    const prev = effectContainer[className];
    effectContainer[className] = { ...prev, [key]: str };
    effectCounter++;
  };
}

class User {
  @Effect('decorating User.name property')
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}

const alice = new User('alice');
console.log(alice.name)
const beth = new User('beth');
console.log(beth.name)
const arisu = new JapaneseUser('有栖');
console.log(arisu.name)

console.log(effectContainer);
console.log(effectCounter);
$ yarn ts-node src/main.ts
alice
beth
有栖
{ User: { name: 'decorating User.name property' } }
1

User Class のインスタンスは子 Class 含め複数回生成していますが、 Decorator 関数は 1度しか呼ばれていません。
コンパイル済みの以下のコードを見ると、 Class 宣言の後に1度評価されているのみであることが分かります。

dist/main.js
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let effectContainer = {};
function Effect(str) {
    return function (target, key) {
        const className = target.constructor.name;
        const prev = effectContainer[className];
        effectContainer[className] = { ...prev, [key]: str };
    };
}
class User {
    constructor(name) {
        this.name = name;
    }
}
__decorate([
    Effect('decorating name property')
], User.prototype, "name", void 0);
class JapaneseUser extends User {
    constructor(name) {
        super(name);
        this.name = name;
    }
    sayHello() {
        console.log(`こんにちは、私は${this.name}です。`);
    }
}
const alice = new User('alice');
const beth = new User('beth');
const arisu = new JapaneseUser('有栖');
console.log(effectContainer);
//# sourceMappingURL=main.js.map

この例で上げたのが副作用であるのは、 Decorator 関数の返す関数が取れる引数が 2つのみであり、 PropertyDescripter が存在しないため、呼び出し元の Class に対して何も操作することが現状できないためです。
子 Class に対して定義した場合は、新規の定義として実行されます。

class JapaneseUser extends User {
  @Effect('decorating JapaneseUser.name property')
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}
$ yarn ts-node src/main.ts
alice
beth
有栖
{
  User: { name: 'decorating User.name property' },
  JapaneseUser: { name: 'decorating JapaneseUser.name property' } 
}
2

class-validator の Decorator の挙動

class-validator では上記の Property Decorator を使用して定義しますが、その際に Class 名とプロパティ名を Container に記録しています
内部では instanceof による比較をしているようであるため、 Decorator の定義を継承したような挙動に見えます。

備考: instanceof と子クラスについて

該当する Class のインスタンスであるかの比較に instanceof を使用すると、その子孫クラスと比較した場合も true となります。

class User {}
const user = new User()
user instanceof User //=> true
class ExUser extends User {}
const exUser = new ExUser()
exUser instanceof User //=>true

子孫クラスであることを明確に区別したい場合は、 Class 名を取得して比較するのが良いです。

user.constructor.name === exUser.constructor.name //=> false

おわりに

この記事では NestJS で多様される Decorator を継承した場合の挙動について説明しました。
Decorator の仕様はまだ安定していないため、今後挙動が変わる可能性がある点はくれぐれもご留意ください。

明日は @potato4dGitHub Actions を利用した NestJS アプリケーションの Google AppEngine への自動デプロイ です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript の Decorator と継承

この記事は NestJS アドベントカレンダー 2019 14 日目の記事です。
寝込んでいたため遅くなり申し訳ありません。

はじめに

この記事では NestJS で多用される Decorator を継承した場合の挙動について説明します。
サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day14-decorator-and-inheritance

なお、環境は執筆時点での Node.js の LTS である v12.13.x を前提とします。
また、この Decorator の挙動は ECMA Script 仕様として定義されていない Decorator に対して、TypeScript 3.7.x 時点での実装による挙動であるため、将来的に仕様の策定・変更に伴い TypeScript コンパイラの挙動が変更になる可能性があります。

結論

メソッドの Decorator 情報は継承されます。オーバーライドで切ることができます。

プロパティの Decorator は Class の定義時にしか評価されません。
しかし評価時にクラス名をキーにして container に副作用を与え、 instanceof で比較を行うようなライブラリでは、 instanceof は子 Class に対して親 Class と比較しても true となる(後述します)ため、継承しているような挙動に見えることがあります。

詳しくは以下で、 Method Decorator と Property Decorator に分けて説明します。

Method Decorator の挙動を追う

Decorator を定義した Class を継承した、 Decorator を直接定義していない Class のインスタンスを生成し、 Validator を定義した sayHello() を呼びます。
以下で定義する @LogProxy() は、関数の実行前後にログを出力する簡単な Decorator 関数です。

src/main.ts
function LogProxy(when: 'before' | 'after' | 'all') {
  return function(_target: any, key: string, desc: PropertyDescriptor) {
    const prev = desc.value;
    const next = function() {
      if (when === 'before' || when === 'all') {
        console.log(`${this.name}.${key} will start.`);
      }
      const result = prev.apply(this);
      if (when === 'after' || when === 'all') {
        console.log(`${this.name}.${key} has finished.`);
      }
      return result;
    };
    desc.value = next;
  };
}

class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  @LogProxy('all')
  sayHello() {
    console.log(`Hello, I am ${this.name}.`);
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}

const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
有栖.sayHello will start.
Hello, I am 有栖.
有栖.sayHello has finished.

コンパイルされた Decorator がどのような挙動をしているのか確認するため、コンパイルされたファイルを読みます。なお、 target は es2019 ですが、 2015 以降であれば Decorator 周りはほぼ変わらないようです。

dist/main.js
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function LogProxy(when) {
    return function (_target, key, desc) {
        const prev = desc.value;
        const next = function () {
            if (when === 'before' || when === 'all') {
                console.log(`${this.name}.${key} will start.`);
            }
            const result = prev.apply(this);
            if (when === 'after' || when === 'all') {
                console.log(`${this.name}.${key} has finished.`);
            }
            return result;
        };
        desc.value = next;
    };
}

class User {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`Hello, I am ${this.name}.`);
    }
}
__decorate([
    LogProxy('all')
], User.prototype, "sayHello", null);
class JapaneseUser extends User {
    constructor(name) {
        super(name);
        this.name = name;
    }
}
const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
//# sourceMappingURL=main.js.map

全てを読まずとも、 __decorate が User.prototype の name に、 decorator 関数を食わせた値を再代入していることが分かります。
下の継承している側の Class では特に defineProperty をしているわけではないので、 Decorator の影響を受け続けています。

そのため、継承した Class でオーバーライドした場合には Decorator の影響は受けません。

src/main.ts
function LogProxy(when: 'before' | 'after' | 'all') {
  return function(_target: any, key: string, desc: PropertyDescriptor) {
    const prev = desc.value;
    const next = function() {
      if (when === 'before' || when === 'all') {
        console.log(`${this.name}.${key} will start.`);
      }
      const result = prev.apply(this);
      if (when === 'after' || when === 'all') {
        console.log(`${this.name}.${key} has finished.`);
      }
      return result;
    };
    desc.value = next;
  };
}

class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  @LogProxy('all')
  sayHello() {
    console.log(`Hello, I am ${this.name}.`);
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }

  sayHello() {
    console.log(`こんにちは、私は${this.name}です。`);
  }

}

const alice = new User('alice');
alice.sayHello();
const arisu = new JapaneseUser('有栖');
arisu.sayHello();
$ yarn ts-node src/main.ts

alice.sayHello will start.
Hello, I am alice.
alice.sayHello has finished.
こんにちは、私は有栖です。

Property Decorator の挙動を追う

同様に、 Decorator を定義した Class とその子 Class を定義します。
以下で定義する @Effect() は、呼び出し時に呼び出し元とプロパティ名、引数を Container に記録する副作用を持つ Decorator 関数です。

src/main.ts
let effectContainer = {};
let effectCounter = 0;

function Effect(str: string) {
  return function(target: any, key: string) {
    const className = target.constructor.name;
    const prev = effectContainer[className];
    effectContainer[className] = { ...prev, [key]: str };
    effectCounter++;
  };
}

class User {
  @Effect('decorating User.name property')
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class JapaneseUser extends User {
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}

const alice = new User('alice');
console.log(alice.name)
const beth = new User('beth');
console.log(beth.name)
const arisu = new JapaneseUser('有栖');
console.log(arisu.name)

console.log(effectContainer);
console.log(effectCounter);
$ yarn ts-node src/main.ts
alice
beth
有栖
{ User: { name: 'decorating User.name property' } }
1

User Class のインスタンスは子 Class 含め複数回生成していますが、 Decorator 関数は 1度しか呼ばれていません。
コンパイル済みの以下のコードを見ると、 Class 宣言の後に1度評価されているのみであることが分かります。

dist/main.js
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let effectContainer = {};
function Effect(str) {
    return function (target, key) {
        const className = target.constructor.name;
        const prev = effectContainer[className];
        effectContainer[className] = { ...prev, [key]: str };
    };
}
class User {
    constructor(name) {
        this.name = name;
    }
}
__decorate([
    Effect('decorating name property')
], User.prototype, "name", void 0);
class JapaneseUser extends User {
    constructor(name) {
        super(name);
        this.name = name;
    }
    sayHello() {
        console.log(`こんにちは、私は${this.name}です。`);
    }
}
const alice = new User('alice');
const beth = new User('beth');
const arisu = new JapaneseUser('有栖');
console.log(effectContainer);
//# sourceMappingURL=main.js.map

この例で上げたのが副作用であるのは、 Decorator 関数の返す関数が取れる引数が 2つのみであり、 PropertyDescripter が存在しないため、呼び出し元の Class に対して何も操作することが現状できないためです。
子 Class に対して定義した場合は、新規の定義として実行されます。

class JapaneseUser extends User {
  @Effect('decorating JapaneseUser.name property')
  name: string;

  constructor(name: string) {
    super(name);
    this.name = name;
  }
}
$ yarn ts-node src/main.ts
alice
beth
有栖
{
  User: { name: 'decorating User.name property' },
  JapaneseUser: { name: 'decorating JapaneseUser.name property' } 
}
2

class-validator の Decorator の挙動

class-validator では上記の Property Decorator を使用して定義しますが、その際に Class 名とプロパティ名を Container に記録しています
内部では instanceof による比較をしているようであるため、 Decorator の定義を継承したような挙動に見えます。

備考: instanceof と子クラスについて

該当する Class のインスタンスであるかの比較に instanceof を使用すると、その子孫クラスと比較した場合も true となります。

class User {}
const user = new User()
user instanceof User //=> true
class ExUser extends User {}
const exUser = new ExUser()
exUser instanceof User //=>true

子孫クラスであることを明確に区別したい場合は、 Class 名を取得して比較するのが良いです。

user.constructor.name === exUser.constructor.name //=> false

おわりに

この記事では NestJS で多様される Decorator を継承した場合の挙動について説明しました。
Decorator の仕様はまだ安定していないため、今後挙動が変わる可能性がある点はくれぐれもご留意ください。

明日は @potato4dGitHub Actions を利用した NestJS アプリケーションの Google AppEngine への自動デプロイ です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【kintone】 APIトークンの自動生成

まえがき

レコードやアプリの操作は、kintone REST APIでほぼほぼ実装できます。
しかし、通知やAPIトークンといった一部設定値については、kintone REST APIでサポートされておりません。
cybozu developer networkのナレッジノート記事で紹介したPuppeteerを使うと、そんなかゆい所に手が届いたりします。
今回は、APIトークンを自動生成する例を紹介します。

デモ

Puppeteerを使って、APIトークンを自動生成します。
demo.gif
生成したAPIトークンが利用可能か、kintone コマンドラインツールを用いて検証しています。
オプションの「-d」にドメイン名、「-a」にアプリID、「-t」にAPIトークンを指定しています。

コード

Puppeteer、readline-syncを利用しています。 npm等を用いてインストールしてください。

・sample.js (実行コード)

(async () => {
  const readline = require('readline-sync'); //readline-syncの読み込み
  const puppeteer = require('puppeteer'); //puppeteerの読み込み

  //設定値
  const domain = '****.cybozu.com'; //kintoneのドメイン
  const basicUser = false; //Basic認証のユーザー名(設定していない場合はfalse)
  const basicPassword = false; //Basic認証のユーザー名(設定していない場合はfalse)
  const user = '****'; //kintoneのログインユーザー名
  const password = '****'; //kintoneのログインパスワード

  const appId = readline.questionInt('App ID: '); //アプリのID
  const accessRights = []; //追加するAPIトークンに与えるアクセス権
  accessRights[0] = readline.keyInYN('Record view right: ');
  accessRights[1] = readline.keyInYN('Record add right: ');
  accessRights[2] = readline.keyInYN('Record edit right: ');
  accessRights[3] = readline.keyInYN('Record delete right: ');
  accessRights[4] = readline.keyInYN('App edit right: ');

  const browser = await puppeteer.launch(); //ブラウザ起動
  const page = await browser.newPage();
  if(basicUser && basicPassword){ //Basic認証(設定している場合)
    await page.setExtraHTTPHeaders({
      Authorization: `Basic ${new Buffer.from(`${basicUser}:${basicPassword}`).toString('base64')}`
    });
  }
  await page.goto(`https://${domain}/k/admin/app/apitoken?app=${appId}`); //ログインページへ遷移
  await page.type('input[name="username"]', user); //kintoneのユーザー名入力
  await page.type('input[name="password"]', password); //kintoneのログインパスワード入力
  await (await page.$('.login-button')).click(); //「ログイン」ボタンをクリック

  await page.waitForNavigation({waitUntil: "domcontentloaded"}); //APIトークンの設定ページへの遷移を待機
  await page.waitFor('.gaia-admin-app-apitoken-add'); //「生成する」ボタンの描画を待機
  await (await page.$('.gaia-admin-app-apitoken-add')).click(); //「生成する」ボタンをクリック
  const apitoken = await page.evaluate((accessRights) => (
    new Promise(resolve => {
      new MutationObserver(() => { //APIトークンの生成を待機
        const targetRow = document.getElementsByClassName('gaia-admin-app-apitoken-row')[0];
        accessRights.forEach((accessRight, index) => {
          targetRow.childNodes[1].getElementsByTagName('input')[index].checked = accessRight; //APIトークンのアクセス権の変更
        });
        resolve(targetRow.childNodes[0].innerText); //APIトークンの取得
      }).observe(document.getElementsByClassName('gaia-admin-app-apitoken-table-body')[0], {childList: true, subtree: true});
    })
  ), accessRights);
  await (await page.$('.button-submit-cybozu')).click(); //「保存」ボタンをクリック

  await page.waitForNavigation({waitUntil: "domcontentloaded"}); //アプリの設定ページへの遷移を待機
  await page.waitFor('.gaia-admin-app-deploy-button'); //「アプリを更新」ボタンの描画を待機
  await (await page.$('.gaia-admin-app-deploy-button')).click(); //「アプリを更新」ボタンをクリック
  await page.waitFor('.gaia-argoui-dialog-buttons-default-tutorial'); //「OK」ボタンの描画を待機
  await (await page.$('.gaia-argoui-dialog-buttons-default-tutorial')).click(); //「OK」ボタンをクリック

  browser.close(); //ブラウザ停止

  console.log(apitoken); //コマンドラインにAPIトークンを出力
})();

kintoneの仕様変更により、正しく動作しなくなる可能性があります。
予めご了承ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE WORKS Bot APIをひと通り触ってみる(node.js)#1

最近、アンタッチャブルが奇跡の復活を遂げましたね。嬉しい限りです。:grin:
どうも@shotamako 初めての投稿 & LINEWORKS Advent Calendar 2019 / 20日目の記事です。

本記事では、LINE WORKS Bot のメッセージ受信 API をnode.jsでひと通り触ってみたいと思います。

0. はじめに

記事の流れになります。

  1. こんなの作ります
  2. 環境準備
  3. 作ってみる
  4. 動かしてみる
  5. まとめ

1. こんなの作ります

LINE WORKS Botのメッセージ受信(callback)には下表のタイプが存在し、そのタイプによって送信するメッセージを切替えるBotを作ります。

callbackタイプ 説明
message メンバーからのメッセージ
join Bot が複数人トークルームに招待された
leave Bot が複数人トークルームから退室した
joined メンバーが Bot のいるトークルームに参加した
left メンバーが Bot のいるトークルームから退室した
postback postback タイプのメッセージ

Botの利用開始 (message)

04_welcomebot.png

メンバーからのメッセージを受信 (message)

05_sendmessage.png

Bot が複数人トークルームに招待された (join)

13_addpanda.png

Bot が複数人トークルームから退室した (leave)

Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。

メンバーが Bot のいるトークルームに参加した (joined)

10_addpanda.png

メンバーが Bot のいるトークルームから退室した (left)

09_outpanda.png

postback タイプのメッセージ (postback)

postbackは、次回の記事で書きま〜す。

2. 環境準備

まずは LINE WORKS Bot API の利用準備と開発環境を整えたいと思います。

LINE WORKS Bot APIの利用準備

  1. LINE WORKS の Developer Console で(今回開発する)Botサーバーが LINE WORKS と通信するために必要な接続情報の発行とBotの登録を行います。
    ↓こちらの記事を参考に作業していただければと思います。
    LINE WORKSで初めてのBot開発!(前編) の「Developer ConsoleでAPIを使うための設定とBotを登録する
    ※Bot登録の際に指定する Callback URL は、ngrokを利用して取得するとローカルデバッグができるのでとっても便利です。
    (記事:ローカル環境で LINEWORKS Bot を動かす話が大変参考になりました)

  2. LINE WORKS の管理画面で、Developer Console で登録したBotをメンバーが利用できる様に設定します。
    ↓こちらの記事を参考に作業していただければと思います。
    LINE WORKSで初めてのBot開発!(後編) の「Botを公開し利用する

開発環境

  • VS Code:IDE
  • node.js+Express:Botサーバー
  • dotenv:アプリケーションの環境変数定義
  • ngrok:ローカルデバッグ

node.jsでいろいろpackageを利用してますが省略します。

4. 作ってみる

まずはメインのjs (server.js)

server.jsでは、リクエストの受け口、改竄防止や LINE WORKS の Access token を取得するプログラムを書いてます。
(Access token をちゃんと管理してません。近々に対応策を書こうと思います。。。DBが必要になるな〜。。。)
あと、BotMessageServiceクラスのsendメソッドでメッセージ送受信を制御してます。

server.js
const express = require('express');
const app = express();
require('dotenv').config();
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const request = require('request');
const BotMessageService = require('./BotMessageService');

var port = process.env.PORT || 3000
app.listen(port, function() {
    console.log('To view your app, open this link in your browser: http://localhost:' + port);
});

app.use(express.json({verify:(req, res, buf, encoding) => {
  // メッセージの改ざん防止
  const data = crypto.createHmac('sha256', process.env.API_ID).update(buf).digest('base64');
  const signature = req.headers['x-works-signature'];

  if (data !== signature) {
    throw 'NOT_MATCHED signature';
  }
}}));

/* 
* 疎通確認API
*/
app.get('/', function (req, res) {
  res.send('起動してます!');
});

/**
 * LINE WORKS からのメッセージを受信するAPI
 */
app.post('/callback', async function (req, res, next) {
  res.sendStatus(200);
  try {
    const serverToken = await getServerTokenFromLineWorks();
    const botMessageService = new BotMessageService(serverToken);
    await botMessageService.send(req.body);
  } catch (error) {
    return next(error);
  }
});

/** 
 * JWTを作成します。
 * @return {string} JWT
 */
function createJWT() {
  const iss = process.env.SERVER_ID;
  const iat = Math.floor(Date.now() / 1000);
  const exp = iat + 60;
  const cert = process.env.PRIVATE_KEY;

  return new Promise((resolve, reject) => {
    jwt.sign({ iss: iss, iat: iat, exp: exp }, cert, { algorithm: 'RS256' }, (error, jwtData) => {
      if (error) {
        console.log('createJWT error')
        reject(error);
      } else {
        resolve(jwtData);
      }
    });
  });
}

/**
 * LINE WORKS から Serverトークンを取得します。
 * @return {string} Serverトークン
 */
async function getServerTokenFromLineWorks() {
  const jwtData = await createJWT();
  // 注意:
  // このサンプルでは有効期限1時間のServerトークンをリクエストが来るたびに LINE WORKS から取得しています。
  // 本番稼働時は、取得したServerトークンを NoSQL データベース等に保持し、
  // 有効期限が過ぎた場合にのみ、再度 LINE WORKS から取得するように実装してください。
  const postdata = {
    url: `https://authapi.worksmobile.com/b/${process.env.API_ID}/server/token`,
    headers : {
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    },
    form: {
      grant_type: encodeURIComponent('urn:ietf:params:oauth:grant-type:jwt-bearer'),
      assertion: jwtData
    }
  };
  return new Promise((resolve, reject) => {
    // LINE WORKS から Serverトークンを取得リクエスト
    request.post(postdata, (error, response, body) => {
      if (error) {
        console.log('getServerTokenFromLineWorks error');
        reject(error);
      } else {
        resolve(JSON.parse(body).access_token);
      }
    });
  });
}

メッセージの送受信制御 (BotMessageService.js)

ベタ書きですが、BotMessageServiceクラスの_getResponse(callbackEvent)メソッドでメッセージの受信内容を解釈して、送信するメッセージ内容を決定してます。
実際 LINE WORKSにメッセージを送信しているところは、send(callbackEvent)メソッドです。

BotMessageService.js
const request = require('request');

const CALL_BACK_TYPE = {
  message : 'message',
  join : 'join',
  leave : 'leave',
  joined : 'joined',
  left : 'left',
  postback : 'postback',
};

/**
 * BotMessageServiceクラス
 */
module.exports = class BotMessageService {

  /**
   * BotMessageServiceを初期化します。
   * @param {string} serverToken Serverトークン
   */
  constructor (serverToken) {
    this._serverToken = serverToken;
  }

  /**
   * LINE WORKS にBotメッセージを送信します。
   * @param {object} callbackEvent リクエストのコールバックイベント
   */
  async send(callbackEvent) {
    let res = this._getResponse(callbackEvent);
    if (!res) {
      return;
    }
    return new Promise((resolve, reject) => {
      // LINE WORKS にメッセージを送信するリクエスト
      request.post(this._createMessage(res), (error, response, body) => {
          if (error) {
            console.log('BotService.send error')
            console.log(error);
          }
          console.log(body);
          // 揉み消してます!
          resolve();
      });
    });
  }

  /**
   * LINE WORKS に送信するBotメッセージを作成して返します。
   * @param {object} res レスポンスデータ
   */
  _createMessage(res) {
    return {
      url: `https://apis.worksmobile.com/${process.env.API_ID}/message/sendMessage/v2`,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8',
        consumerKey: process.env.CONSUMER_KEY,
        Authorization: `Bearer ${this._serverToken}`
      },
      json: res
    };
  }

  /**
   * メンバーIDを連結して返します。
   * @param {Array} memberList メンバーリスト
   */
  _buildMember(memberList) {
    let result = '';
    if (memberList) {
      memberList.forEach(m => {
        if (result.length > 0) {
          result += ',';
        }
        result += m;
      });
    }
    return result;
  }

  /**
   * Bot実装部
   * @param {object} callbackEvent リクエストのコールバックイベント
   * @return {string} レスポンスメッセージ
   */
  _getResponse(callbackEvent) {
    console.log(callbackEvent);

    let res = {
      botNo : Number(process.env.BOT_NO),
    };
    if (callbackEvent.source.roomId) {
      // 受信したデータにトークルームIDがある場合は、送信先にも同じトークルームIDを指定します。
      res.roomId = callbackEvent.source.roomId;
    } else {
      // トークルームIDがない場合はBotとユーザーとの1:1のチャットです。
      res.accountId = callbackEvent.source.accountId;
    }

    switch (callbackEvent.type) {
      case CALL_BACK_TYPE.message:
        // メンバーからのメッセージ
        if (callbackEvent.content.postback == 'start') {
          // メンバーと Bot との初回トークを開始する画面で「利用開始」を押すと、自動的に「利用開始」というメッセージがコールされる
          console.log(`start`);
          res.content = { type: 'text', text: 'ト〜クルームに〜〜。ボトやまが〜くる〜!' };
          return res;
        }

        console.log(CALL_BACK_TYPE.message);
        res.content = { type: 'text', text: 'からの〜〜〜。' };
        break;

      case CALL_BACK_TYPE.join:
        // Bot が複数人トークルームに招待された
        // このイベントがコールされるタイミング
        //  ・API を使って Bot がトークルームを生成した
        //  ・メンバーが Bot を含むトークルームを作成した
        //  ・Bot が複数人のトークルームに招待された
        // ※メンバー1人と Bot のトークルームに他のメンバーを招待したらjoinがコールされる(最初の1回だけ)
        //  招待したメンバーを退会させ、再度他のメンバーを招待するとjoinedがコールされるこれ仕様?
        //  たぶん、メンバー1人と Botの場合、トークルームIDが払い出されてないことが原因だろう。。。
        console.log(CALL_BACK_TYPE.join);
        res.content = { type: 'text', text: 'うぃーん!' };
        break;

      case CALL_BACK_TYPE.leave:
        // Bot が複数人トークルームから退室した
        // このイベントがコールされるタイミング
        //  ・API を使って Bot を退室させた
        //  ・メンバーが Bot をトークルームから退室させた
        //  ・何らかの理由で複数人のトークルームが解散した
        console.log(CALL_BACK_TYPE.leave);
        break;

      case CALL_BACK_TYPE.joined: {
        // メンバーが Bot のいるトークルームに参加した
        // このイベントがコールされるタイミング
        //  ・Bot がトークルームを生成した
        //  ・Bot が他のメンバーをトークルームに招待した
        //  ・トークルームにいるメンバーが他のメンバーを招待した
        console.log(CALL_BACK_TYPE.joined);
        res.content = { type: 'text', text: `${this._buildMember(callbackEvent.memberList)} いらっしゃいませ〜そのせつは〜` };
        break;
      }

      case CALL_BACK_TYPE.left: {
        // メンバーが Bot のいるトークルームから退室した
        // このイベントがコールされるタイミング
        //  ・Bot が属するトークルームでメンバーが自ら退室した、もしくは退室させられた
        //  ・何らかの理由でトークルームが解散した
        console.log(CALL_BACK_TYPE.left);
        res.content = { type: 'text', text: `${this._buildMember(callbackEvent.memberList)} そうなります?` };
        break;
      }

      case CALL_BACK_TYPE.postback:
        // postback タイプのメッセージ
        // このイベントがコールされるタイミング
        //  ・メッセージ送信(Carousel)
        //  ・メッセージ送信(Image Carousel)
        //  ・トークリッチメニュー
        // ※次回の記事で作り込みます。
        console.log(CALL_BACK_TYPE.postback);
        break;

      default:
        console.log('知らないコールバックですね。。。');
        return null;
    }

    return res;
  }
}

環境変数 (.env)

「LINE WORKS Bot APIの利用準備」で発行した接続情報を設定する。

.env
API_ID="API ID"
CONSUMER_KEY="Consumer key"
SERVER_ID="Server ID"
PRIVATE_KEY="認証キー"
BOT_NO="Bot No"

5. 動かしてみる

いざデバッグ開始!

1. VS Code のターミナルでプログラムが使用している node.js の package をインストール

VsCodeTerminal
npm install

2. デバッグボタン(F5)クリック!

vscode.png

3. http 3000 で ngrok 起動!

Terminal
ngrok http 3000

ngrok2.png
※ ForwardingもとのURLが変わった場合は、Developer Console で Botの Callback URL の変更を必ずしてください。

4. スマフォを手に持って LINE WORKS を動かす

アクター

  • Bot:ボトやま
  • メンバー1:栗井 (スマートフォンを操作している人)
  • メンバー2:パンダD

シナリオ1:ボトやまの利用を開始してみる (message)

01_roomlist.png
02_selectbot.png
03_openbot.png
04_welcomebot.png

想定通りのうごきですね。

シナリオ2:栗井からメッセージを送信してみる (message)

05_sendmessage.png

想定通りのうごきですね。

シナリオ3:栗井とパンダDのトークルームにボトやまを招待してみる (join)

11_kaproom.png
12_addmenber.png
02_selectbot.png
13_addpanda.png

想定通りのうごきですね。

シナリオ4:栗井/パンダD/ボトやまのトークルームからボトやまを退室させてみる (leave)

Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。
(consoleログが出力されます)

シナリオ5:栗井とボトやまのトークルームにパンダDを招待してみる (joined)

06_addmenber.png
07_selectpanda.png

↓ あれ? Callback タイプが joined だと思いきや、join みたいですね。。。想定と違う。。。(なので、一度パンダDに退室してもらう)

08_openpanda.png

↓パンダD退室

09_outpanda.png

↓もう一度招待する

10_addpanda.png

↑これが想定通りの動き(なんでだろう。。。)

シナリオ6:栗井/パンダD/ボトやまのトークルームからパンダDを退室させてみる (left)

09_outpanda.png

想定通りの動き

6. まとめ

LINE WORKS Bot APIのメッセージ受信部分の動作をひと通り確認できました。(一部気になるところがありますが。。。)
今回作成たコードは GitHub の line-works-bot01-node で公開してま〜す。(issueがあればお知らせください。修正します。)

次回クリスマス記事もがんばります!メッセージ送信API!

Link

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1ヶ月間JavaScript一色の開発環境に飛び込んで学んだこと

こちらは、PLAID Advent Calendar 2019 22日目の記事です。

11月にプレイドにエンジニアとして入社しました。

これまでは2年ほど機械学習関連プロダクトのプロトタイプ開発を行っており、モデリングや前処理の部分はPython, 軽くフロントを作るために時々Vue.jsを書いているという感じだったのですが、プレイドではフロントエンドにVue.js, サーバーサイドにNode.js を採用しており、JavaScript一色の開発環境の中で日々奮闘しています。(しかも、実際に稼働しているプロダクションの開発は初という)

そんな環境の中で1ヶ月間程開発をしてきたにあたって、詰まったこと・学んだことを共有していきたいと思います。ポエムっぽくなる気がしていますが、お付き合いいただけると嬉しいです。

対象読者

  • JavaScriptの超基本文法はわかるけど、JavaScriptでのプロダクト開発経験が乏しい人
  • 転職して間もない、かつ経験が少ないエンジニアがどのようなところで苦労しているか気になる人

そもそも非同期処理分からない

まずぶち当たった壁がこれでした。Node.jsというものが初めてだったということもあり、割と困惑しました

言葉で説明するよりコードで見てもらった方が良いと思うのですが、

function increment(a) {
  setTimeout(function() {
    return a + 1;
  }, 1000); //何かしら時間のかかる処理(今回は擬似的にsetTimeout)
}

const result = increment(2);
console.log(result); //undefined

??? ちゃんと変数に代入してるのに? Pythonなら以下のように問題なく動くのに、、

import time
def increment(a):
    time.sleep(1)
    return a + 1

result = increment(2)
print(hoge) #3 1秒後に表示される

さらに、実際にはundefinedでない時もあり、非同期処理という概念が自分の中にない状態だと余計に戸惑いました。

解決方法は数多の先人たちが残してくれているので、詳細には説明しませんが、以下のような例が考えられると思います。

function increment(a) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve(a + 1);
    }, 1000);
  });
}

increment(2).then(result => {
  console.log(result); // 3
});

人によって書き方がバラバラ

「ふむふむ、"非同期処理完全に理解した"」となった瞬間にぶち当たったのがこれです。

//aとcallbackを受け取って、何かしらの処理をしてcallbackを返す関数
function hoge_callback(a, cb) {
  //何かしらの処理
  cb(err, data);
}

//bを受け取って、インクリメントした値をPromiseオブジェクトとして返す関数
function fuga_promise(b) {
  return new Promise((resolve, reject) => {
    resolve(b + 1);
  });
}

//hoge_callbackを呼ぶ時
hoge_callback(a, (err, data) => {
  console.log(data);
});

//fuga_promiseを呼ぶ時
fuga_callback(b).then(result => {
  console.log(result);
});

コード例の通り、callbackを返す関数とPromiseオブジェクトを返す関数が混在している状態であったため、呼び出し方もそれらに合った方法にしなければいけず、Node.js初心者の自分にとってはかなり困惑してしまいました。他にもasync/await とかもあるので、非同期処理周り分かり合える気がしないなという気持ちになってました。。もういっそcallback地獄でも良いわという魔が刺しそうになることも。

逆に() => {}function() {}って書き方違うだけで中身同じやんと思ってたら、thisの挙動が違うという。。
詳しくは関数とthis · JavaScript Primer #jsprimer を参考にしてもらえればと思います。

const hoge = {
  a: "fuga",
  b: function() {
    return this.a;
  },
  c: () => {
    return this.a;
  }
};
console.log(hoge.b()); //fuga
console.log(hoge.c()); //undefined

どこにロジックを依存させるか

JavaScript依存の話ではありませんが、ここも自分の中で大きく学べたことなので、共有したいと思います。

現在開発しているプロダクトのコードはざっくりと以下のような構成になっています。

basic_architecture.png

  • Vue Component: 画面表示を担う
  • Store: 画面表示のためのデータの状態を管理する
  • Router: VueとLogic間の情報の受け渡しを行う
  • Logic: DBから取得した情報を処理してVue側に伝える/Vue側で変更した情報を処理してDBに保存する
  • MongoDB: 名前そのままにDB

以上の構成に則り実装し、想定通りの動作をすることが確認できたので、レビューに出したところ、レビューワーの方に「Rounter内にロジックは書かないほうが良いよ」と言われました。自分の書いたコードでは、情報の受け渡しを行っているRouterの部分にロジックを書いていたのです。

レビューワー曰く、ここは情報を受け渡すだけのところだからロジックは後ろに移動したほうが良いとのこと。いわゆるMVCモデルでいうコントローラの部分でロジックは書いちゃいけないということかと納得しました。

前職ではプロトタイプ開発ということもあったからか、そこらへんをあまり意識せずに書いていたので、自分の中では大きな学びになりました。

最後に

以上が1ヶ月間JavaScript一色の開発環境に飛び込んで学んだことになります。

初歩的な話ばかりだったかもしれませんが、おそらく初心者が詰まりやすいポイントだと思うので、誰かしらの助けになればと思います。
O'Reilly Japan - Node.jsデザインパターン 第2版もかなり参考になったので、共有しておきます。

他にもVue.jsのStoreパターンって良いな〜とか、Our seeking to stable k8s deployment.で紹介されているようなマイクロサービスって便利だな〜とか、いろいろ感じたことはあるのですが、またどこかで学んだことを共有できればと思います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自転車のパワーをRaspberryPiで可視化する

はじめに

近年、自転車(ロードバイク)では、ペダルを踏んだ力を「パワー」として数値化(単位はワット:W)できる「パワーメーター」が普及しつつあります。

4iiii.png
$\small{出典:4iii-innovations}$

筆者のパワーメーターは上記の画像の様にクランクに接着されていて、ペダルを踏んだ際のクランクの歪み度合いを、パワーに変換しています。

そんなパワーメーターを利用したバーチャルサイクリングアプリ「Zwift」が、自転車乗りの世界で人気沸騰中です。
今回はZwiftの紹介とRaspberryPiを利用し、出力したパワーをLEDの色で可視化する方法を紹介します。

Zwiftとは?

zwift.png

Zwift はMMO(Massively Multiplayer Online)ゲーム形式のサイクリング・ランニングトレーニングプログラム。世界中の参加者が仮想世界の中でトレーニングしたり、競争したりすることができる。
(Wikipediaより)

上記画像の様に、自転車をローラーという器具に固定し、パワーメーターの情報を元にZwiftのアバターをバーチャル世界で走らせます。
高性能なローラーだとパワーの検出、仮想世界の斜度に応じた負荷の変化、路面の凸凹を再現などもでき、より実走に近い感覚でサイクリングができます。

天候や時間に左右されることなく、世界中の人とレースを楽しみながら、トレーニングもできてしまう夢のようなアプリです。

やりたいこと

今回やりたいのはRaspberryPiを利用し、自分が出力しているパワー(以下、"パワー値")をLEDの色で可視化することです。

Zwiftで世界中の猛者と戦う中、必死に漕いでいると意識が朦朧としてきます。
そんな状況ではワット数を気にしている余裕がありません。そこでパワー値を色で表現すればわかりやすいのではないか、というのが今回の動機です。

用意するもの

実装(ハードウェア)

RaspberryPiとWS2812の配線は下記の通りです。
パワーメーターとの通信はANT+で行います。データの受信にはAnt受信ドングルを用います。
ws2812.png

実装(ソフトウェア)

以下に、ソフトウェアの実装手順と実行コマンドを示します。
まずはRaspberryPiにログインし、インストールを行っていきましょう。

USBライブラリのインストール

Ant受信ドングルを接続し、接続情報を取得、IDをメモしておきます。
結果は下記以外のUSB機器の情報も表示されます。「Dynastream Innovations」がAnt受信ドングルの情報です。

command
lsusb
Bus 001 Device 00X: ID 0fcf:1008 Dynastream Innovations, Inc. 

USBライブラリのインストールを行います。

command
sudo apt-get install -y libusb-1.0-0-dev libudev-dev

設定ファイルを作成します。IDが異なっていた場合は修正して下さい。

command
echo SUBSYSTEM=="usb", ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1008", RUN+="/sbin/modprobe usbserial vendor=0x0fcf product=0x1008", MODE="0666", OWNER="pi", GROUP="root" | sudo tee /etc/udev/rules.d/garmin-ant2.rules 

Node.js、各種ライブラリのインストール

Node.jsに加えて、下記のライブラリもインストールします。
・ant-plus (npm GitHub)
・rpi-ws281x-v2 (npm GitHub)

commnad
sudo apt install -y nodejs npm  // Node.jsのインストール

mkdir -p ~/project/ant-ws2812  // Project作成
cd ~/project/ant-ws2812
npm init
npm install ant-plus // ライブラリのインストール
npm install rpi-ws281x-v2 --save

ソース

インストールが終わったところで、ようやく実装です!
GitHubにあるサンプルコードを元に実装していきます。
WS2812のサンプルコードはモジュールのLED数とGPIOのピン位置が異なるため変更します。

~/project/ant-ws2812/index.js
const Ant = require("ant-plus");
const ws281x = require("rpi-ws281x-v2")
const stick = new Ant.GarminStick2();
const sensor = new Ant.HeartRateSensor(stick);
const bicyclePowerSensor = new Ant.BicyclePowerSensor(stick);
// パワー範囲の定義
const Zone1 = 160;
const Zone2 = 210;
const Zone3 = 250;
const Zone4 = 300;
const Zone5 = 350;

let count = 0;

class Light {

        constructor() {
                this.config = {};
                this.config.leds = 8; // モジュールのLED数変更
                this.config.dma = 5;
                this.config.brightness = 100;
                this.config.gpio = 12; // GPIOピン変更
                this.config.strip = 'grb';
                ws281x.configure(this.config);
        }

        run(str) {
                var pixels = new Uint32Array(this.config.leds);
                switch (str){
                        // LED色の定義
                        case "red": red = 255; break;
                        case "orange": red = 255; green = 165; break;
                        case "yellow": red = 255; green = 255; break;
                        case "green": green = 255; break;
                        case "blue": blue = 255; break;
                        case "off": red = 0; green = 0; blue = 0; break;
                }
                var color = (red << 16) | (green << 8)| blue;

                for (var i = 0; i < this.config.leds; i++)
                        pixels[i] = color;

                // Render to strip
                ws281x.render(pixels);
        }

};

var light = new Light();
bicyclePowerSensor.on("powerData", function(data) { 
                count += 1;
                var num;
                num = data.ComputedHeartRate;
                // パワー値毎のLED色分け
                if(num < Zone1){
                        light.run("blue");
                }else if(Zone1 <= num && num < Zone2){
                        light.run("green")
                }else if(Zone2 <= num && num < Zone3){
                        light.run("yellow")
                }else if(Zone3 <= num && num < Zone4){
                        light.run("orange")
                }else if(Zone5 <= num){
                        light.run("red")
                }
                // パワー値の出力
                console.log(count, data.DeviceID, num);
                });

stick.on("startup", function() {
                console.log("on start up");
                bicyclePowerSensor.attach(0, 0);
                });
async function main() {
        if (!stick.open()) {
                console.log("Stick not found!");
                return;
        }
}
main();

実行

sudo node index.js

結果

以下が、実装後、Zwiftプレイ中のLEDの様子を撮影した動画です!

分かりにくいですが、画面左上(スマホの左)のLEDの色がパワー値に応じて変化しました!
※RaspberryPiの数値はリアルタイムで検出しているのに対し、Zwiftは3秒平均のパワーを表示させているためZwiftのパワー値は遅れて表示されています。

まとめ

Node.jsのライブラリを使用することで、簡単にパワー値の検出・LEDの点灯を実装することができました。これで日々のトレーニングに生かせそうです。また、LEDの色を変化させる事のない様に漕げば、パワーにムラの出ないペダリングも習得できそうです。
今後は、ディスプレイを接続して心拍など様々な情報も出力してみたいと思います。

最後までお読みいただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureFunctionsをJavascriptで構築する時の嵌りどころ

はじめに

Javascript初心者がAzureFunctionsの開発を行った際に嵌ったポイントをお伝えしたいと思います。

前提

実際の案件概要は以下のような感じです

  • 概要:検証用のPoCアプリの開発
  • 構成:SPA(Vue.js) + AzureFunctions + CosmosDBでのサーバレスアプリケーション(基本PaaSで)
  • ローカル環境:Windows + VisualStudioCode
  • 言語:Javascript(フロントエンドとバックエンドで統一したかった)
  • 開発時期:2019年8月~2019年11月 ※本記事で書いている内容は現在時点でも修正されているかもしれませんし、今後も修正される予定のものも多いので、あくまで参考程度に見てもらえると助かります

本記事で触れるテーマ

  • ベースOSの選択
  • ローカル環境構築について
    • Proxyを突破する技術
  • Lintの設定
  • Bindingsの落とし穴

ベースOSの選択

Functionsのリソースを作成する際、事情がない限りはWindowsを指定する形で良いと思います。
Azureの開発速度は凄まじいですが、優先順位はとしてはどうしてもWindows > Linux、 C# > その他の言語になると思います。
(ドキュメント量や対応済・未対応のIssueの数から鑑みて)

嵌ったポイント①:LinuxOSを選択

個人的にはLinuxOSの方が慣れていたので、今回の開発でもLinuxを使おうと思い、インフラ担当の方にLinuxでリソースを作製していただきました。

WebApps

AzureWebAppsでフロントエンドを作成した際、ベースOSとしてlinuxを選択してはまりました。
【事象】アプリをリリースしても資産が置き換わらない
【対策】資産の中に入ってコマンド打って修正する
【参考URL】https://stackoverflow.com/questions/54236862/cannot-get-index-html-azure-linux-web-app

上記のstackoverflowを見てヒイヒイ言いながらexpressの設定をしたおかげで、なんとか対応すること出来ました

Functions

今回CosmosDBを利用する必要があったため、Functionsを利用する際に
ExtensionBandlesをインストールする必要がありました。
【参考URL】https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-register

LinuxベースだとHTTPTriggerとTimerTriggerの選択肢しか表示されないため、手動インストールができません(Windowsだと出てきます)

色々試行錯誤しましたが解決策がわからなかったため、結局リソースをWindowsベースで作り直してもらうことで対応しました。

将来的には改善されるかもしれませんが、特別な理由がない限り現時点でのベースOSはWindowsで良いと思います(そもそもOSを意識していては、折角のサーバレスが台なしな感じもしますが・・・)

ローカル環境構築について

次に嵌まったポイントとしてはローカル開発環境の構築です。

基本的にIDEとしてはVisualStudioCode(以下vscode)を使います。
素直にMicrosoft製品で固めます。

基本的にはazure-functions-core-toolsnpm installすればローカルのデバッグはできるのですが、
やはりというか、認証Proxyで引っかかりました。
認証Proxyの仕様については各社によって差異があるので、以下のやり方で突破可能か不明ですが、ある程度参考にしてもらえたらと思います。

認証Proxyのどこで引っかかるのか

AzureFunctionsを起動する際、ローカルでもクラウドでもExtension.Bandle(C#で書かれたライブラリ)が必要になります。
AzureFunctions自体は様々な言語で開発することができますが、基本的にはExtension.Bandleのライブラリを呼び出して様々な処理を実行させています。

  1. func startコマンドでFunctionsを起動する
  2. ローカルでExtension.Bandleを探す
  3. ない場合、ダウンロードしに行く
  4. Extension.Bandleのダウンロードが完了次第、各Functionが起動する

認証Proxy化で引っかかるのは3の部分です。
Extension.Bandleをダウンロードする処理はazure-functions-core-toolsの機能の中で行われるのですが、これがおそらくC#で書かれている処理のため、ローカルで環境変数にProxyを設定しても処理が途中で止まり、タイムアウトとなってしまいました。

ExtensionBundlesの入手方法

ではどうしたか、というとAzure Functionsの実行に必要なExtensionBundles(前回梅田がStorage経由でお渡ししたモジュール群)をネット経由で入手する方法が分かりました。
以下のGitHubのページからリリース版のzipファイルをローカルにダウンロードし、ローカルのC:\Users\[ユーザ名]\AppData\Local\Temp\Functions\ExtensionBundlesの配下(パスについてはユーザごとに変わる可能性がありますので、VSCODEの画面からご確認ください)に解凍後のフォルダを置くことで、ExtensionBundleを用いたFunctionsの実行が可能となります

https://github.com/Azure/azure-functions-extension-bundles/releases

これでなんとかローカルでデバッグができる状況まで漕ぎつけました。

Lintについて

ESLintのデフォルトのルールを採用すると、Azure Functionsの性質上、必ず通せないエラーが発生します。
https://github.com/eslint/eslint/issues/11723

Functionsを記述する場合、引数として、contextを指定します。これはお約束

module.exports = async function(context, req) {

そしてDBやBLOB、Queueに結果を出力しようとすると以下のような記述になります。

context.bindings.XXXX = AAAAA

このような記述をするとLintのルールの再代入にあたるため、エラーとなってしまいます

今回私はルールの方を無効化する対応をとってしまいましたが、
functionsをreturn文で終わらせる書き方をすると、ここのLintのルールを守りつつコードを書くことが可能になると思います。

【参考URL】https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-node

Bindingsの落とし穴

AzureFunctionsを使う上ではfunction.jsonに記述するBindings指定が本当に便利です。
わざわざDBコネクションを確立したり、オブジェクトストレージのパスを気にしたりみたなところに気を遣わずにプログラムを作成することができます。
※Bindingsを使わない場合、各関数にライブラリをimportしないといけなくなるため、処理を軽量化することが難しくなります
【参考URL】https://github.com/MicrosoftDocs/azure-docs.ja-jp/blob/master/articles/azure-functions/functions-bindings-cosmosdb.md#tab/javascript

function.jsonでCosmosDBにつなぐ際、SQL文を書かずともある程度データを選択して抽出することができます。

書き方は以下のようになります。

{
"type": "cosmosDB",
"name": "index.jsの中で使う変数名",
"databaseName": "DB名",
"collectionName": "コレクション名",
"connectionStringSetting": "DB接続文字列",
"direction": "in",
"id": "{hugahuga}",
"partitionKey": "{hogehoge}"
},

この書き方をすると、Pertition=hogehoge内のid=hugahugaのデータをオブジェクトとして抽出してくれるので非常に便利です。

が、実際に動かすうえでは以下のような制約がありました
- id指定とPertition指定は同時にやらないといけない
- id指定のみだと実行時にエラーとなる
- Pertition指定のみだとエラーにならないが、実際はPertition指定が効いてないため、全件検索と同じになってしまう

対象データをidで一意に特定する場合は、idとpertitionKeyの併用は良いかもしれませんが、素直にSqlQueryパラメーターを使った方が良いかもしれません

最後に

つらつらとめんどくさかったことばかり書いていますが、
サーバレスによるアプリ開発は開発が不慣れな人でもある程度の速度を持って開発できるし、インフラやミドルレイヤを気にしなくてよいなど良いこともたくさんあります!
Javascriptは個人的にはとっつきやすい言語(フロントが絡むとまた別かもしれませんが)でした。
ぜひとも、Javascript + AzureFunctionsによるサーバレスデビューを!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Use Async Hooks to monitor asynchronous operations

これがNode.js Advent Calendar 2019 19日目の記事です。宜しくお願いいたします。

Use Async Hooks to monitor asynchronous operations

非同期がJavascriptの特徴で、そして難しいどころです。この記事がNodeJSのAsync Hooks機能で非同期操作を監視することを紹介したいです。

私がJia Liと申します。非同期について大好きで、angular/zone.jsという非同期管理のライブラリのCode Ownerです、一応NodeJSのAsyncHooksのCollaboratorとしてZone.jsとAsyncHooksの連携もやっています。

この記事がNodeJSのAsyncHooksの機能を紹介したいです。

なんで非同期を監視したいですか?

機能を紹介する前に、まずUseCaseを紹介したいです。

  1. 非同期性能を計測
  2. 非同期のDebug・Tracing
  3. 非同期ユーザ操作の追跡
  4. 非同期でContext/Namespaceのようなものがほしい

ということです。

性能の計測

例えば、下記のコードでの非同期操作の性能を計測したい。

function heavyWork() {
  for (let i = 0; i < 10000; i++) {
  }
}
function asyncOperation1() {
  setTimeout(heavyWork);
}
function asyncOperation2() {
  setTimeout(heavyWork);
}
function testAsync() {
  asyncOperation1();
  asyncOperation2();
}

const start = Date.now();
testAsync();
console.log('performance is', Date.now() - start);

非同期の関数の場合、この書き方で性能を正しく計測できないです。
でも、正しく計測したい場合、下記のような面倒なソースを書かないといけないです。
もちろん改善の余地があると思いますが、でもどうしてもいろいろな非同期のための処理を
入れる必要があります。

function heavyWork() {
  for (let i = 0; i < 100000; i++) {
    let m = i * i;
  }
}

let total = 0;
let asyncOperation1Done = false;
let asyncOperation2Done = false;

function calculatePerformance(target) {
  const start = Date.now();
  target();
  return Date.now() - start;
}
function asyncOperation1() {
  setTimeout(() => {
    total += calculatePerformance(heavyWork);
    asyncOperation1Done = true;
    if (asyncOperation1Done && asyncOperation2Done) {
      doneFn();
    }
  });
}
function asyncOperation2(doneFn) {
  setTimeout(() => {
    total += calculatePerformance(heavyWork);
    asyncOperation2Done = true;
    if (asyncOperation1Done && asyncOperation2Done) {
      doneFn();
    }
  });
}

function testAsync(doneFn) {
  asyncOperation1(doneFn);
  asyncOperation2(doneFn);
}

testAsync(() => {
  console.log('total performance is', total);
});

このようなコードで拡張性もないし、非同期のCallbackにいじる必要もあるし、基本てきには現実ではないです。実際ほしいのはこのような感じのコードです。

performanceWatcher.watch(() => {
  testAsync();
});

つまり、実際のアプリコードを触らなくて、非同期のLife CycleをInterceptできる方法がほしいです。
AsyncHooksが非同期のLife Cycleでいろいろ Callbackを提供しました、それを利用したら、非同期の監視などができます。
提供されたCallbackが
- init(asyncId, type, triggerAsyncId, resource): 非同期操作が初期化、Scheduleするとき呼び出されます。
- before(asyncId): 非同期のCallbackを実行する前に呼び出されます。
- after(asyncId): 非同期のCallbackを実行したあとで呼び出されます。
- destroy(asyncId): 非同期のリソースが開放するとき、呼び出されます。
- promiseResolve: Promiseのresolve関数を呼び出すときこのCallbackを呼び出されます。Promiseだけ有効です。

になります。
実際がこのようなイメージです。

setTimeout(() => { // init is called
// before is called
doSomething();
// after is called
});
// destroyed will be called when VM decide to GC the resource

AsyncHooksを有効するため、下記のような設定が必要です。

const async_hooks = require('async_hooks');
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });
asyncHook.enable();

無効するには、

asyncHook.disable();

そしたら、PerformanceWatcherをAsyncHooksで実装してみます。

const async_hooks = require('async_hooks');

const asyncHook = async_hooks.createHook({init, before, after, destroy});
asyncHook.enable();
let total = 0;
let tasks = [];
let perfByAsyncId = {};

let doneCallback;

function init(asyncId, type, triggerAsyncId, resource) {
  tasks.push(asyncId);
}
function before(asyncId) {
  perfByAsyncId[asyncId] = {start: Date.now()};
}
function after(asyncId) {
  perfByAsyncId[asyncId] = {perf: Date.now() - perfByAsyncId[asyncId].start};
  for (let i = 0; i < tasks.length; i++) {
    if (tasks[i] === asyncId) {
      tasks.splice(i, 1);
      break;
    }
  }
  if (tasks.length === 0) {
    Object.keys(perfByAsyncId).forEach(id => {
      total += perfByAsyncId[id].perf;
    });
    doneCallback(total);
  }
}
function destroy(asyncId) {}

function start(targetFn, doneFn) {
  total = 0;
  tasks = [];
  doneCallback = doneFn;
  perfByAsyncId = {};
  targetFn();
}

module.exports.start = start;

計測するとき、使い方が下記のようになります。
javascript
const p = require('./performance_watcher');
p.start(testAsync, (total) => {
log('total performance is', total);
});

performanceWatcherについて、説明させていただきます。

// init
function init(asyncId, type, triggerAsyncId, resource) {
  tasks.push(asyncId);
}

initのとき、tasksという配列で非同期Idを記録します。この配列がEmptyではないと、なにか非同期の操作がまだ終わっていないという意味です。

function before(asyncId) {
  perfByAsyncId[asyncId] = {start: Date.now()};
}

非同期のCallbackが実行した前に、開始時間を記録します。

function after(asyncId) {
  perfByAsyncId[asyncId] = {perf: Date.now() - perfByAsyncId[asyncId].start};
  for (let i = 0; i < tasks.length; i++) {
    if (tasks[i] === asyncId) {
      tasks.splice(i, 1);
      break;
    }
  }
  if (tasks.length === 0) {
    Object.keys(perfByAsyncId).forEach(id => {
      total += perfByAsyncId[id].perf;
    });
    doneCallback(total);
  }
}

非同期のCallbackが実行したあとで、かかる時間を計測して、そして、Tasksの配列からこの非同期Idを削除します。もしTasksの配列がEmptyの場合、すべての非同期が完了ということになります。そして、すべての非同期Callbackかかる時間をプラスして、最後出力します。

このような感じで、実際のテストの対象を触らなくても、非同期の性能計測ができます。
性能計測だけではなく、いろいろな非同期の監視とかもできますので、とっても面白いツールです。

Zone.js

私がメインでZone.jsをメンテしますが、Zone.jsがやっていることがAsyncHooksと似てます。非同期の監視と管理です、AsyncHooksと違って、Zone.jsがHooksだけではなく、Interceptorです。AsyncHooksが通知だけ受けられますが、Zone.jsが通知を受けるだけではなく、非同期のBehaviorを変わることもできます。皆さんが興味があったら、ぜひ@Quramyさんの記事を読んでください。

どうもありがとうございました、まだ宜しくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CAMPFIREのページをモニタリングしてクラウドファンディングの状況をウォッチする - スクレイピング編

クラウドファンディングプラットフォームの大手CAMPFIREさんのWebサイトをスクレイピングして、ファンディング中のプロジェクトの現在の進捗や、パトロン数などをウォッチしたいと思います。

今回はスクレイピング編です。

対象とするSPARKSチャンネル

今回はCAMPFIREの中でも、プロトアウトスタジオ x CAMPFIREで現在開催中のSPARKS by BOOSTER STUDIOのチャンネルを対象にスクレイピングしてみます。

Sparks - https://camp-fire.jp/channels/sparks

スクリーンショット 2019-12-18 1.49.15.png

環境など

  • Node.js 13.3.0
  • axios 0.19.0

Node.js 13.3.0で試してみていて、ES Modulesな記述(import)にしてみています。
もし真似しようとしてエラーが出る人は冒頭のimport文をconst axios = require('axios');に書き換えて従来の読み込みにしましょう。

npm init -y
npm i axios

こんな感じで事前に準備はしておきます。

まずは要素の特定

Chromeのディベロッパーツール(右クリック->検証)で各要素の抜き出しをしてみます。

スクリーンショット 2019-12-18 1.48.49.png

何となく、class="box"やdata_project_idの辺りの記述で引っ張ってこれるかもしれないとアタリを付けてみます。

campfire.js
'use strict';

import axios from 'axios';

const CF_URL = `https://camp-fire.jp/channels/sparks`;

axios.get(CF_URL).then(res => {
    const bodyall = res.data;
    let parts = bodyall.split('data_project_id='); // `data_project_id=`の箇所でスプリット
    parts.shift(); //HTML全体の最初を削除
    console.log(parts[0]);
})

この時点でこんなHTMLが取得できます。

"210634"><div class="box-in"><div class="box-thumbnail"><a href="/projects/view/210634?list=channel_sparks"><img class="lazyload" data-srcset="https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=320&amp;h=213&amp;fit=clip&amp;auto=format 320w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=414&amp;h=276&amp;fit=clip&amp;auto=format 414w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=768&amp;h=512&amp;fit=clip&amp;auto=format 768w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=960&amp;h=639&amp;fit=clip&amp;auto=format 960w, https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=1024&amp;h=682&amp;fit=clip&amp;auto=format 1024w" data-sizes="100vw" data-src="https://static.camp-fire.jp/uploads/project_version/image/329602/f8330bbc-b104-437f-a1b0-773052ddd9d6.png?ixlib=rails-2.1.4&amp;w=1120&amp;h=746&amp;fit=clip&amp;auto=format"></a></div><div class="box-title"><a title="EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ" href="/projects/view/210634?list=channel_sparks"><h4>EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ</h4></a><div class="sub"><p>「いつでもどこでも気軽にタイムラプス動画を。」タイムラプス動画を撮影したことはありますか?確かに素敵な動画が撮れますが、撮影中ずっとスマホが使えず、気軽に撮るこ...</p></div></div><div class="box-date sp-none"><div class="category"><a href="/projects/category/technology"><i class="fa fa-tag"></i> テクノロジー・ガジェット</a></div><div class="ownner"><a href="/profile/takeaship"><i class="fa fa-user"></i> takeaship</a></div></div><div class="meter">
<div class="meter-in"><div class="bar" style="width: 0%;"><span>0%</span></div></div>
<span>0%</span>
</div><div class="overview">
<div class="total" data-js="money-unit">
<small>現在</small>0円</div>
<div class="rest">
<small>パトロン</small>0人</div>
<div class="per">
<small>残り</small>9日</div>
</div></div></div><div class="box  " 

細々と情報を抜き出し

<small>残り</small>9日</div>

9の部分だったり

<small>パトロン</small>0人</div>

0の部分だったりを正規表現で抜き出します。

campfire.js
'use strict';

import axios from 'axios';

const CF_URL = `https://camp-fire.jp/channels/sparks`;

axios.get(CF_URL).then(res => {
    const bodyall = res.data;
    let parts = bodyall.split('data_project_id=');
    parts.shift();

    const part = parts[0]; //0件目

    const project = {};
    project.percentage = part.match(/<span>(.*?)%<\/span>/)[1]; //達成率
    project.yen = part.match(/<\/small>(.*?)円<\/div>/)[1]; //円
    project.patron = part.match(/パトロン<\/small>(.*?)人<\/div>/)[1]; //パトロン数
    project.remaining_days = part.match(/残り<\/small>(.*?)日<\/div>/)[1]; //残り日数
    project.title = part.match(/<a title="(.*?)" href="/)[1]; //タイトル
    project.description = part.match(/<div class="sub"><p>(.*?)...\/p><\/div><\/div>/)[1]; //概要
    project.link = 'https://camp-fire.jp' + part.match(/div class="box-thumbnail"><a href="(.*?)">/)[1]; //リンク

    console.log(project);
})
node campfire.js
(node:2508) ExperimentalWarning: The ESM module loader is experimental.
{
  percentage: '0',
  yen: '0',
  patron: '0',
  remaining_days: '9',
  title: 'EZ-Lapse いつでもどこでも気軽にタイムラプス動画を撮影できるカメラ',
  description: '「いつでもどこでも気軽にタイムラプス動画を。」タイムラプス動画を撮影したことはありますか?確かに素敵な動画が撮れますが、撮影中ずっとスマホが使えず、気軽に撮るこ.',
  link: 'https://camp-fire.jp/projects/view/210634?list=channel_sparks'
}

こんな雰囲気ですね。

あとは複数プロジェクト分処理を回す

campfire.js
'use strict';

import axios from 'axios';

const CF_URL = `https://camp-fire.jp/channels/sparks`;

axios.get(CF_URL).then(res => {
    const bodyall = res.data;
    let parts = bodyall.split('data_project_id=');
    parts.shift();

    for (let i = 0, len = parts.length; i < len; i++) {
        const part = parts[i];
        const project = {};
        project.percentage = part.match(/<span>(.*?)%<\/span>/)[1]; //達成率
        project.yen = part.match(/<\/small>(.*?)円<\/div>/)[1]; //円
        project.patron = part.match(/パトロン<\/small>(.*?)人<\/div>/)[1]; //パトロン数
        project.remaining_days = part.match(/残り<\/small>(.*?)日<\/div>/)[1]; //残り日数
        project.title = part.match(/<a title="(.*?)" href="/)[1]; //タイトル
        project.description = (part.match(/class="sub"><p>(.*?)<\/p>/)) ? (part.match(/class="sub"><p>(.*?)<\/p>/)[1]) : ''; //概要
        project.link = 'https://camp-fire.jp' + part.match(/div class="box-thumbnail"><a href="(.*?)">/)[1]; //リンク
        // project.image = part.match(/class=" lazyloaded" data-srcret="(.*?)">/)[1]; //画像
        console.log(project);      
    }

})
$ node campfire.js

・
・
・
{
  percentage: '56',
  yen: '5,700',
  patron: '7',
  remaining_days: '4',
  title: '【おかたづけ】こどもが自分で片付けしたくなるIoTおもちゃ箱',
  description: 'こどもがおもちゃを散らかしっぱなしにして困っているお母さんお父さん大助かり!おもちゃ箱を電子工作して子供が自分で片付けしたくなります!',
  link: 'https://camp-fire.jp/projects/view/211804?list=channel_sparks'
}
{
  percentage: '10',
  yen: '13,000',
  patron: '11',
  remaining_days: '4',
  title: 'オフィスワーカー向け 座り過ぎを解決するクッション CiliCill シリシル',
  description: '世界一の「座りすぎ大国」日本そんな日本人特有の問題を解決したい!座ってる時間がわかるクッション CiliCill -シリシル-を作りました。一日の中で自分がどの...',
  link: 'https://camp-fire.jp/projects/view/207642?list=channel_sparks'
}

こんな感じでプロジェクトの情報を抜き出せました。

次回

次はこれを定期実行させる&チャット通知させる予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む