برنامه نویسی

Type-safe S3 پرس و جوها را با Kysely انتخاب کنید

S3 Select یک ویژگی Amazon S3 است که امکان بازیابی زیرمجموعه های محتوای S3 Objects را از طریق عبارات SQL فراهم می کند. می‌توانید از بندهایی مانند SELECT و WHERE برای واکشی داده‌ها از فایل‌های CSV، JSON یا Apache Parquet استفاده کنید، حتی اگر با GZIP و/یا رمزگذاری شده در سمت سرور فشرده شده باشند.

S3 Select برای استفاده ساده، مقرون به صرفه است و بسته به درخواست شما می تواند عملکرد برنامه شما را به شدت بهبود بخشد. به طور کلی یک افزودنی عالی به جعبه ابزار توسعه دهنده بدون سرور است، به ویژه زمانی که:

  • شما باید به حجم زیادی از داده ها دسترسی داشته باشید (که آن را برای راه حل های ذخیره سازی دیگر مانند DynamoDB نامناسب می کند)
  • شما باید آن را به روشی پیچیده و/یا پویا پرس و جو و فیلتر کنید
  • شما نیازی به استفاده از بند JOIN ندارید (زیرا S3 Select فقط می تواند یک فایل را پرس و جو کند) یا برای به روز رسانی یک رکورد از داده ها (به آن S3 Select می گویند نه S3 Insert 🙂)

در این مقاله، نحوه اجرای دستورات S3 Select در تایپ اسکریپت را یاد خواهیم گرفت و چگونه می توانیم DevX و ایمنی نوع خود را با استفاده از Kysely بهبود دهیم.

پرس و جو با S3 Select

بیایید بگوییم که ما یک DB از پوکمون ها به شکل CSV داریم که در جایی در یک سطل S3 ذخیره شده است:

id ; name      ; customName ; type     ; level ; generation
1  ; pikachu   ;            ; electric ; 42    ; 1
2  ; charizard ;            ; fire     ; 54    ; 1
3  ; meganium  ; plantyDino ; grass    ; 26    ; 2
...
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر بخواهیم پوکمون های آتش را از نسل 1 و 2 بازیابی کنیم چه؟ خوب، S3 Select اجازه دهید این کار را با کوئری زیر انجام دهیم:

import { S3Client, SelectObjectContentCommand } from '@aws-sdk/client-s3';

export const s3Client = new S3Client({
  region: 'us-east-1', // <= Your region here
});

const { Payload } = await s3Client.send(
  new SelectObjectContentCommand({
    // 👇 Those first params are required but don't mind them
    ExpressionType: 'SQL',
    OutputSerialization: {
      JSON: {
        RecordDelimiter: ',',
      },
    },
    // 👇 Those depends on the CSV
    InputSerialization: {
      CSV: {
        FileHeaderInfo: 'USE',
        FieldDelimiter: ';',
        QuoteCharacter: '"',
      },
    },
    // 👇 Those are the most important
    Bucket: 'my-super-bucket-name',
    Key: 'pokedex.csv',
    Expression: `
            select "id", "name", "customName", "type" as "pokemonType", "level"
                from "S3Object"
                where
                    "generation" in ('1', '2')
                    and "type" = 'fire'
      `,
  }),
);

// Note that this command requires the s3:GetObject permission
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این عالی است و همه چیز به جز ما می توانیم آن را بهتر کنیم:

  • 📝 برای استفاده از پاسخ باید مقداری تجزیه سفارشی اضافه کنیم Payload
  • 👍 ما می‌توانیم از Kysely برای تولید عبارت SQL به روشی ایمن و سازگار با devX استفاده کنیم
  • 🌈 همچنین می توانیم از آن برای تایپ نتیجه دستور استفاده کنیم

تجزیه پاسخ پرس و جو

این Payload یک شی جاوا اسکریپت ساده نیست بلکه نمونه ای از آن است AsyncInterable. حالا چه جهنمی است AsyncIterable شما از من بپرسید؟ خوب، می‌توانید به دنبال اسناد رسمی Async Iterator در MDN بگردید یا فقط می‌توانید از پرسیدن سؤال خودداری کنید و از راهنما زیر استفاده کنید:

import type { SelectObjectContentEventStream } from '@aws-sdk/client-s3';

// 👇 TextDecoder is a native Node/Browser class
const textDecoder = new TextDecoder();

const parseS3SelectEventStream = async (
  s3SelectEventStream:
    | AsyncIterable<SelectObjectContentEventStream>
    | undefined,
): Promise<unknown[]> => {
  if (!s3SelectEventStream) {
    return [];
  }

  const stringifiedJSONOutputs: string[] = [];

  for await (const event of s3SelectEventStream) {
    if (event.Records) {
      const stringifiedJSONOutput = textDecoder.decode(event.Records.Payload);
      stringifiedJSONOutputs.push(stringifiedJSONOutput);
    }
  }

  const rows = JSON.parse(
    '[' + stringifiedJSONOutputs.join('').slice(0, -1) + ']',
  ) as unknown[];

  return rows;
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا می توانیم خروجی را به صورت زیر تجزیه کنیم:

const { Payload: s3SelectEventStream } = await s3Client.send(
  new SelectObjectContentCommand({
    // ...
  }),
);

// 🎉 Ta-da!
const myPokemons: unknown[] = await parseS3SelectEventStream(
  s3SelectEventStream,
);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساخت پرس و جو با Kysely

بیایید با آن روبرو شویم، نوشتن پرس و جوهای SQL با دست دردناک و مستعد خطا است. علاوه بر این، اگر CSV ناگهان تغییر شکل دهد، خوب نیست که یک خطای نوع داشته باشیم؟

اینجاست که Kysely به کمک می آید: Kysely یک سازنده پرس و جوی SQL تایپ اسکریپت سازگار با نوع و devX است. این برای کار با PostgreSQL و MySQL طراحی شده بود، اما چند کلاس را در معرض دید قرار می‌دهد که می‌توانند به ما اجازه نوشتن پرس‌و‌جوها را بدون اتصال به یک پایگاه داده رابطه‌ای واقعی بدهند.

بیایید با طراحی نوع CSV خود شروع کنیم:

enum PokemonType {
  Water = 'water',
  Grass = 'grass',
  Fire = 'fire',
  // ...
}

interface PokemonCSV {
  id: string;
  name: string;
  customName?: string;
  type: PokemonType;
  // 👇 In CSVs everything is a string
  level: string;
  generation: '1' | '2'; // ...up to 9
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بعد، بیایید Kysely را نصب کنیم و a را نمونه کنیم Kysely پایگاه داده:

# npm
npm install kysely

# yarn
yarn add kysely
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

import {
  Kysely,
  DummyDriver,
  SqliteAdapter,
  SqliteIntrospector,
  SqliteQueryCompiler,
} from 'kysely';

interface Database {
  S3Object: PokemonCSV;
}

const db = new Kysely<Database>({
  dialect: {
    createAdapter: () => new SqliteAdapter(),
    createDriver: () => new DummyDriver(),
    createIntrospector: ($db: Kysely<unknown>) => new SqliteIntrospector($db),
    createQueryCompiler: () => new SqliteQueryCompiler(),
  },
});

// That’s it 🎉
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساده، نه؟ اکنون، بیایید همان پرس و جو را بنویسیم، اما در حالی که از ایمنی نوع و تکمیل خودکار لذت می بریم:

const kyselyQuery = db
  .selectFrom('S3Object')
  .select([
    'id',
    'name',
    'customName',
    // 🙌 You can rename columns as you like
    'type as pokemonType',
    'level',
    // 💥 Will trigger an error:
    'unexistingColumn',
  ])
  // 🙌 Every method is type-safe!
  .where('generation', 'in', ['1', '2'])
  .where('type', '=', PokemonType.Fire);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای محافظت از ما در برابر تزریق های نامناسب SQL، Kysely مستقیماً عبارت SQL را در اختیار ما قرار نمی دهد، بلکه یک sql رشته و parameters آرایه:

const {
  sql, // 👈 SQL query with '?' as placeholders
  parameters, // 👈 Array of parameters
} = kyselyQuery.compile();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که S3 Select پارامترها را در API خود نمی پذیرد، باید خودمان پارامترها را هیدراته کنیم:

const dangerouslyHydrateSQLParameters = (
  sql: string,
  parameters: readonly unknown[],
): string => {
  for (const parameter of parameters) {
    sql = sql.replace('?', `'${String(parameter)}'`);
  }

  return sql;
};

const { sql, parameters } = kyselyQuery.compile();

// ⛔️ **BE SURE TO VALIDATE DYNAMIC PARAMETERS FIRST** ⛔️
const sqlExpression = dangerouslyHydrateSQLParameters(sql, parameters);
console.log(sqlExpression);

// 👇 We retrieve the same expression as above:
// select "id", "name", "customName", "type" as "pokemonType", "level"
//   from "S3Object"
//   where
//     "generation" in ('1', '2')
//     and "type" = 'fire'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما فقط باید آن را به دستور S3 Select و voilà خود ارائه دهیم! انجام شد!

تقریباً: توجه کنید که ما فقط عبارت پرس و جوی SQL خود را تایپ کردیم! در مورد پاسخی که از S3 می آید چطور؟

استنباط نوع پاسخ

در ابتدا، Kysely نه تنها برای ساخت پرس و جو ساخته شد، بلکه آنها را نیز اجرا کرد. ما فقط می توانیم با بازرسی از نوع استنباط شده بهره مند شویم execute ویژگی پرس و جو Kysely ما. این را می توان با کمک برخی از جادوگری TS انجام داد:

import type { SelectQueryBuilder } from 'kysely';

type QueryResponseRow<
  // 👇 Add a large type constraint
  KyselyQuery extends SelectQueryBuilder<
    Record<string, unknown>,
    string,
    unknown
  >,
> =
  // 👇 Remove the Promise wrapper
  Awaited<
    // 👇 Get the return type of the execute method
    ReturnType<KyselyQuery['execute']>
    // 👇 Unpack the array
  >[number];

type Pokemon = QueryResponseRow<typeof kyselyQuery>;

// 👇 Equivalent to:
type Pokemon = {
  id: string;
  name: string;
  // 👍 customName is indeed possibly undefined
  customName: string | undefined;
  level: string;
  // 🙌 "type" property has been renamed
  pokemonType: PokemonType;
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم از این نوع پاسخ خود استفاده کنیم parseS3SelectEventStream Util و voilà! انجام شد! برای خیر این بار 🙂

نتیجه

هر دو S3 Select و Kysely ابزارهای بسیار خوبی هستند. با پیوستن به هر دوی آنها، می‌توانیم پرس‌وجوهای عملکردی، مقیاس‌پذیر و مقرون‌به‌صرفه را اجرا کنیم و در عین حال از ایمنی و استنتاج نوع قوی و خشک بهره‌مند شویم.

Type-safe S3 پرس و جوها را با Kysely انتخاب کنید

توجه داشته باشید که یک اشکال کوچک وجود دارد، اما: Kysely 120 کیلوبایت به بسته‌های Lambdas شما اضافه می‌کند (برای کمک به من در این مورد 🙌). این مقدار زیادی نیست، اما قابل چشم پوشی هم نیست، زیرا بسته های NodeJS Lambdas بالای 5 مگابایت بر شروع سرد آنها تأثیر منفی می گذارد. بنابراین ممکن است بخواهید افزودن Kysely به بسته‌های خود را مجدداً ارزیابی کنید، اگر درخواست شما اغلب تغییر نمی‌کند.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا