Open3

ValibotでNotionのレスポンスをデシリアライズするユーティリティ

Nakano as a ServiceNakano as a Service
import * as v from "valibot"

const NumberSchema = v.pipe(
  v.object({
    number: v.nullable(v.number()),
  }),
  v.transform((v) => v.number),
)

const UrlSchema = v.pipe(
  v.object({
    url: v.nullable(v.string()),
  }),
  v.transform((v) => v.url),
)

const SelectSchema = v.pipe(
  v.object({
    select: v.nullable(
      v.object({
        name: v.string(),
      }),
    ),
  }),
  v.transform((v) => v.select?.name),
)

const MultiSelectSchema = v.pipe(
  v.object({
    multi_select: v.array(
      v.object({
        name: v.string(),
      }),
    ),
  }),
  v.transform((v) => v.multi_select.map((v) => v.name)),
)

const StatusSchema = v.pipe(
  v.object({
    status: v.nullable(
      v.object({
        name: v.string(),
      }),
    ),
  }),
  v.transform((v) => v.status?.name),
)

const StartDateSchema = v.pipe(
  v.object({
    date: v.nullable(
      v.object({
        start: v.pipe(
          v.string(),
          v.transform((v) => new Date(v)),
        ),
      }),
    ),
  }),
  v.transform((v) => v.date?.start),
)

const DateSchema = v.pipe(
  v.object({
    date: v.nullable(
      v.object({
        start: v.pipe(
          v.string(),
          v.transform((v) => new Date(v)),
        ),
        end: v.nullable(
          v.pipe(
            v.string(),
            v.transform((v) => new Date(v)),
          ),
        ),
      }),
    ),
  }),
  v.transform((v) =>
    v.date
      ? {
          start: v.date.start,
          end: v.date.end,
        }
      : null,
  ),
)

const EmailSchema = v.pipe(
  v.object({
    email: v.nullable(v.string()),
  }),
  v.transform((v) => v.email),
)

const PhoneNumberSchema = v.pipe(
  v.object({
    phone_number: v.nullable(v.string()),
  }),
  v.transform((v) => v.phone_number),
)

const CheckboxSchema = v.pipe(
  v.object({
    checkbox: v.boolean(),
  }),
  v.transform((v) => v.checkbox),
)

const FilesSchema = v.pipe(
  v.object({
    files: v.array(
      v.object({
        file: v.object({
          url: v.string(),
        }),
      }),
    ),
  }),
  v.transform((v) => v.files.map((v) => v.file.url)),
)

const CreatedByNameSchema = v.pipe(
  v.object({
    created_by: v.object({
      name: v.nullish(v.string(), null),
    }),
  }),
  v.transform((v) => v.created_by.name),
)

const CreatedByIdSchema = v.pipe(
  v.object({
    created_by: v.object({
      id: v.string(),
    }),
  }),
  v.transform((v) => v.created_by.id),
)

const CreatedTimeSchema = v.pipe(
  v.object({
    created_time: v.pipe(
      v.string(),
      v.transform((v) => new Date(v)),
    ),
  }),
  v.transform((v) => v.created_time),
)

const LastEditedByNameSchema = v.pipe(
  v.object({
    last_edited_by: v.object({
      name: v.nullish(v.string(), null),
    }),
  }),
  v.transform((v) => v.last_edited_by.name),
)

const LastEditedByIdSchema = v.pipe(
  v.object({
    last_edited_by: v.object({
      id: v.string(),
    }),
  }),
  v.transform((v) => v.last_edited_by.id),
)

const LastEditedTimeSchema = v.pipe(
  v.object({
    last_edited_time: v.pipe(
      v.string(),
      v.transform((v) => new Date(v)),
    ),
  }),
  v.transform((v) => v.last_edited_time),
)

const FormulaSchema = v.pipe(
  v.object({
    formula: v.variant("type", [
      v.object({
        type: v.literal("string"),
        string: v.string(),
      }),
      v.object({
        type: v.literal("date"),
        date: v.nullable(
          v.object({
            start: v.pipe(
              v.string(),
              v.transform((v) => new Date(v)),
            ),
            end: v.nullable(
              v.pipe(
                v.string(),
                v.transform((v) => new Date(v)),
              ),
            ),
          }),
        ),
      }),
      v.object({
        type: v.literal("number"),
        number: v.number(),
      }),
      v.object({
        type: v.literal("boolean"),
        boolean: v.nullable(v.boolean(), false),
      }),
    ]),
  }),
  v.transform((v) => {
    switch (v.formula.type) {
      case "string":
        return v.formula.string
      case "date":
        return v.formula.date
      case "number":
        return v.formula.number
      case "boolean":
        return v.formula.boolean
    }
  }),
)

const UniqueIdSchema = v.pipe(
  v.object({
    unique_id: v.object({
      prefix: v.nullish(v.string(), null),
      number: v.nullish(v.number(), null),
    }),
  }),
  v.transform((v) => v.unique_id),
)

const RichTextArraySchema = v.pipe(
  v.array(
    v.object({
      plain_text: v.string(),
    }),
  ),
  v.transform((v) => v.map((v) => v.plain_text).join("")),
)

const TitleSchema = v.pipe(
  v.object({
    title: RichTextArraySchema,
  }),
  v.transform((v) => v.title),
)

const RichTextSchema = v.pipe(
  v.object({
    rich_text: RichTextArraySchema,
  }),
  v.transform((v) => v.rich_text),
)

const PeopleSchema = v.pipe(
  v.object({
    people: v.array(
      v.object({
        id: v.string(),
        name: v.nullish(v.string(), null),
      }),
    ),
  }),
  v.transform((v) => v.people),
)

const RelationSchema = v.pipe(
  v.object({
    relation: v.array(
      v.pipe(
        v.object({ id: v.string() }),
        v.transform((v) => v.id),
      ),
    ),
  }),
  v.transform((v) => v.relation),
)

export function queryDatabaseResponseSchema<T extends v.GenericSchema>(
  propertiesSchema: T,
) {
  return v.pipe(
    v.object({
      results: v.array(
        v.pipe(
          v.object({
            id: v.string(),
            object: v.literal("page"),
            properties: propertiesSchema,
          }),
        ),
      ),
    }),
    v.transform((v) => v.results),
  )
}
Nakano as a ServiceNakano as a Service
const PropertiesSchema = v.pipe(
  v.object({
    プロジェクト名: TitleSchema,
    NotionタスクデータベースID: RichTextSchema,
    DiscordチャンネルID: RichTextSchema,
  }),
  v.transform((v) => ({
    name: v.プロジェクト名,
    notionTaskDatabaseId: v.NotionタスクデータベースID,
    discordChannelId: v.DiscordチャンネルID,
  })),
)

const QueryDatabaseResponseSchema = v.pipe(
  v.object({
    results: v.array(
      v.pipe(
        v.object({
          id: v.string(),
          object: v.literal("page"),
          properties: PropertiesSchema,
        }),
        v.transform((v) => ({
          id: v.id,
          ...v.properties,
        })),
      ),
    ),
  }),
  v.transform((v) => v.results),
)

const QueryDatabaseResponseSchema = queryDatabaseResponseSchema(
    PropertiesSchema
)