概要
SQLバインドを使う、tediousを使う、は試したので今回はmssqlを使ってSQL Serverに接続を試す。
題材とするSQLとしてJSONが格納されたカラムから配列をレコードとして取り出すことを試す。
データ
{
  "name": "キャラ名",
  "cards": [{ "name": "カード名1"},{ "name": "カード名2"}]
}
ソース
注意書きより、接続は使いまわすソースとする。
 api/src/lib/database.ts
import * as sql from 'mssql';
import type { ISqlType, ConnectionPool, IRecordSet } from 'mssql';
interface ExecuteQueryArgs {
  query: string;
  params: {
    name: string;
    data: unknown;
    type: () => ISqlType;
  }[];
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  logger: (...args: any[]) => void;
}
export default class Database {
  private config: string;
  private poolconnection: ConnectionPool | null = null;
  private connected = false;
  constructor(connectionsString: string) {
    if (connectionsString === '') {
      throw new Error('Database connection string is empty');
    }
    console.log(`Database: config: ${JSON.stringify(connectionsString)}`);
    this.config = connectionsString;
  }
  private async connect() {
    try {
      console.log(`Database connecting...${this.connected}`);
      if (this.connected === false) {
        this.poolconnection = await sql.connect(this.config);
        this.connected = true;
        console.log('Database connection successful');
      } else {
        console.log('Database already connected');
      }
    } catch (error) {
      console.error(`Error connecting to database: ${JSON.stringify(error)}`);
    }
  }
  private async executeQuery<T>(args: ExecuteQueryArgs) {
    await this.connect();
    if (this.poolconnection == null) {
      args.logger('Database connection failed');
      return null;
    }
    const request = this.poolconnection.request();
    for (const { name, data, type } of args.params) {
      request.input(name, type, data);
    }
    args.logger(`Database query start`);
    const result = await request.query<T>(args.query);
    args.logger(
      `Database query end. records: ${result?.recordset?.length} affected: ${result?.rowsAffected[0]}`,
    );
    return result;
  }
  async readAll<T>(args: ExecuteQueryArgs) {
    const result = await this.executeQuery<T>(args);
    if (
      (function hasSets(sets?: unknown[]): sets is IRecordSet<T>[] {
        return sets != null && sets.length !== 0;
      })(result?.recordsets)
    ) {
      return result.recordsets[0];
    }
    return [] as T[];
  }
  async read<T>(args: ExecuteQueryArgs) {
    const result = await this.executeQuery<T>(args);
    const ret = result?.recordset[0];
    return ret;
  }
  async execute(args: ExecuteQueryArgs) {
    await this.connect();
    const result = await this.executeQuery(args);
    return result?.rowsAffected[0];
  }
}
接続文字列の設定。ここの接続文字列はSQLバインドで取得したものと同じものを使用。
api/src/lib/constants.ts
export const connectionStringSetting = 'DATABASE_CONNECTION_STRING';
export const connectionString = process.env[connectionStringSetting] || '';
api/src/functions/getCharacterCards.ts
import {
  app,
  HttpRequest,
  HttpResponseInit,
  InvocationContext,
} from '@azure/functions';
import { connectionString } from '@yakumi-api/lib/constants';
import Database from '@yakumi-api/lib/database';
import * as sql from 'mssql';
// 接続を使いまわすため、関数の外で定義
const db = new Database(connectionString);
export async function getCharacterCards(
  request: HttpRequest,
  context: InvocationContext,
): Promise<HttpResponseInit> {
  context.log(`Http function processed request for url "${request.params.id}"`);
  if (!request.params.id) {
    return {
      status: 400,
      body: JSON.stringify({ message: 'Missing character id' }),
    };
  }
  const ret = await db.readAll<{
    Id: string;
    CharacterName: string;
    CardName: string;
  }>({
    query: `
    WITH CharacterData (Id, CharacterName, Cards) AS (
        select id AS Id
             , JSON_VALUE(data, '$.name') AS CharacterName
             , JSON_QUERY(data, '$.cards') as Cards 
        from YakumiCharacter
        where id = @id
      )
      SELECT Id, CharacterName, CardName
      FROM CharacterData
           CROSS APPLY OPENJSON (Cards) WITH (CardName NVARCHAR(100) '$.name');
    `,
    params: [{ name: 'id', data: request.params.id, type: sql.NVarChar }],
    logger: (args) => context.log(args),
  });
  return { body: JSON.stringify(ret) };
}
app.http('getCharacterCards', {
  route: 'get-character-cards/{id}',
  methods: ['GET'],
  authLevel: 'anonymous',
  handler: getCharacterCards,
});
実行結果
select結果をO/Rマッピングした配列を返してくれる。
[
  {
    "Id": "hoge",
    "CharacterName": "キャラ名",
    "CardName": "カード名1"
  },
  {
    "Id": "hoge",
    "CharacterName": "キャラ名",
    "CardName": "カード名2"
  }
]
参考
組み込み関数を使用した JSON データの検証、クエリ、変更 (SQL Server)
OPNE_JSON - 例 3 - CROSS APPLY を使用してテーブルのセルに格納されている JSON データを行と結合する
WITH common_table_expression (Transact-SQL)
node-mssql
Node.js と mssql npm パッケージを使用して Azure SQL Database に接続し、クエリを実行する
Azure SQL Server の Session が減らない