diff --git a/.env b/.env index cd8af59..97a4f13 100644 --- a/.env +++ b/.env @@ -7,4 +7,9 @@ DATABASE_URL="file:./local.db" # App variables NEXT_PUBLIC_APP_TITLE=Geração 666 | Diretoria -NEXT_PUBLIC_APP_DESCRIPTION=Gerenciador de discografias do Geração 666. \ No newline at end of file +NEXT_PUBLIC_APP_DESCRIPTION=Gerenciador de discografias do Geração 666. + +# GitHub Auth +GITHUB_API_TOKEN= +GITHUB_USER_NAME= +GITHUB_USER_EMAIL= diff --git a/app/api/artist/ArtistAlreadyExistsError.ts b/app/api/artist/ArtistAlreadyExistsError.ts new file mode 100644 index 0000000..1f4a16c --- /dev/null +++ b/app/api/artist/ArtistAlreadyExistsError.ts @@ -0,0 +1,8 @@ +import { ArtistPayload } from "./types" + +export default class ArtistAlreadyExistsError extends Error { + constructor(artist: ArtistPayload) { + super(`Artist already exists: ${artist.name}`) + Object.setPrototypeOf(this, ArtistAlreadyExistsError.prototype) + } +} \ No newline at end of file diff --git a/app/api/artist/artist.service.ts b/app/api/artist/artist.service.ts new file mode 100644 index 0000000..5dc176c --- /dev/null +++ b/app/api/artist/artist.service.ts @@ -0,0 +1,101 @@ +import { ArtistPayload, ReleasePayload } from "./types"; +import createSlug from "@/app/utils/createSlug"; +import prisma from '@/app/lib/prisma' +import ArtistAlreadyExistsError from "./ArtistAlreadyExistsError"; +import { uploadArtworkImage, uploadCoverImage } from "../image/image.service"; + +const connectOrCreateTags = (tags: string[]) => ({ + connectOrCreate: tags.map((name: string) => ({ + where: { name }, + create: { name } + })) +}) + +// TODO: Make releases unique by a conjunction of release name and artists. +// TODO: Make discs and tracks unique too. +const makeRelease = async ( + artistName: string, + artistSlug: string, + release: ReleasePayload, + shaFromContent: boolean = false +) => { + const [artwork] = await uploadArtworkImage(artistName, artistSlug, release, { + shaFromContent + }) + + return { + artwork, + name: release.name, + downloadUrl: release.downloadUrl, + type: { + connect: { name: release.type } + }, + discs: { + create: release.tracks.map((discTracks: string[], discIndex: number) => ({ + number: discIndex + 1, + tracks: { + create: discTracks.map((name, trackIndex) => ({ + name, + number: trackIndex + 1 + })) + } + })) + } + } +} + +const createReleases = async ( + artistName: string, + artistSlug: string, + releases: ReleasePayload[], + shaFromContent: boolean = false +) => { + return { + create: await Promise.all( + releases.map((release) => makeRelease( + artistName, + artistSlug, + release, + shaFromContent + )) + ) + } +} + +export const artistExists = (slug: string) => { + return prisma.artist.findFirst({ + where: { slug } + }) +} + +export const createArtist = async ( + payload: ArtistPayload, + options: { shaFromPayload: boolean } = { shaFromPayload: false } +) => { + const slug = createSlug(payload.name) + if (await artistExists(slug)) { + throw new ArtistAlreadyExistsError(payload) + } + + const [cover] = await uploadCoverImage(payload.name, slug, payload.cover, { + shaFromContent: options.shaFromPayload + }) + + const entity = { + slug, + cover, + name: payload.name, + origin: payload.origin, + tags: connectOrCreateTags(payload.tags), + releases: await createReleases(payload.name, slug, payload.releases, options.shaFromPayload) + } + + return await prisma.artist.upsert({ + where: { + slug, + name: entity.name, + }, + update: { ...entity }, + create: { ...entity } + }) +} \ No newline at end of file diff --git a/app/api/artist/import/route.ts b/app/api/artist/import/route.ts new file mode 100644 index 0000000..4721a17 --- /dev/null +++ b/app/api/artist/import/route.ts @@ -0,0 +1,23 @@ +import { StatusCodes } from "http-status-codes" +import ArtistAlreadyExistsError from "../ArtistAlreadyExistsError" +import { createArtist } from "../artist.service" + +export async function POST(request: Request) { + try { + const payload = await request.json() + const artist = await createArtist(payload, { + shaFromPayload: true + }) + + return Response.json(artist) + } catch (err) { + if (err instanceof ArtistAlreadyExistsError) { + return Response.json( + { error: err.message }, + { status: StatusCodes.CONFLICT } + ) + } + + throw err + } +} \ No newline at end of file diff --git a/app/api/artist/route.ts b/app/api/artist/route.ts new file mode 100644 index 0000000..a0bd4a0 --- /dev/null +++ b/app/api/artist/route.ts @@ -0,0 +1,21 @@ +import { createArtist } from './artist.service' +import ArtistAlreadyExistsError from './ArtistAlreadyExistsError' +import { StatusCodes } from 'http-status-codes' + +export async function POST(request: Request) { + try { + const payload = await request.json() + const artist = await createArtist(payload) + + return Response.json(artist) + } catch (err) { + if (err instanceof ArtistAlreadyExistsError) { + return Response.json( + { error: err.message }, + { status: StatusCodes.CONFLICT } + ) + } + + throw err + } +} \ No newline at end of file diff --git a/app/api/artist/route.tsx b/app/api/artist/route.tsx deleted file mode 100644 index 52407fb..0000000 --- a/app/api/artist/route.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import prisma from '@/app/lib/prisma' - -export async function POST(request: Request) { - const data = await request.json() - - try { - const artist = await prisma.artist.create({ - data: { - ...data, - genres: { - connectOrCreate: data.genres?.map((genre: string) => ({ - where: { name: genre }, - create: { name: genre } - })) - } - } - }) - - return Response.json(artist) - } catch (err) { - // TODO: Handle error correctly - console.log('Error while creating artist:', err) - } finally { - await prisma.$disconnect() - } -} \ No newline at end of file diff --git a/app/api/artist/types.ts b/app/api/artist/types.ts new file mode 100644 index 0000000..431b65b --- /dev/null +++ b/app/api/artist/types.ts @@ -0,0 +1,16 @@ +export type ArtistPayload = { + name: string + origin: string + cover: string + tags: string[] + releases: ReleasePayload[] +} + +export type ReleaseType = 'album' | 'compilation' | 'dvd' | 'ep' | 'live' | 'single' | 'split' +export type ReleasePayload = { + type: ReleaseType + name: string + artwork: string + downloadUrl: string + tracks: string[][] +} \ No newline at end of file diff --git a/app/api/github/GitHubAPILimitReachedError.ts b/app/api/github/GitHubAPILimitReachedError.ts new file mode 100644 index 0000000..edab298 --- /dev/null +++ b/app/api/github/GitHubAPILimitReachedError.ts @@ -0,0 +1,6 @@ +export default class GitHubAPILimitReachedError extends Error { + constructor(message: string = 'GitHub API calls have reached its limit') { + super(message) + Object.setPrototypeOf(this, GitHubAPILimitReachedError.prototype) + } +} \ No newline at end of file diff --git a/app/api/github/github.service.ts b/app/api/github/github.service.ts new file mode 100644 index 0000000..f4c9b7b --- /dev/null +++ b/app/api/github/github.service.ts @@ -0,0 +1,62 @@ +import octokit from '@/app/lib/octokit' +import { RateLimiter, RateLimiterOpts } from 'limiter' +import GitHubAPILimitReachedError from './GitHubAPILimitReachedError' +import { GITHUB_API_RATE_LIMIT, GITHUB_COMMITTER } from '@/app/config/github' +import { GitHubFileUploadParams, GitHubGetFileParams } from './types' +import { RequestError } from 'octokit' + +/** + * Limiter used to keep track of GitHub API calls. + */ +const limiter = new RateLimiter({ + ...GITHUB_API_RATE_LIMIT, + fireImmediately: true +} as RateLimiterOpts) + +/** + * Remove 1 token from the rate limiter. + */ +const removeToken = async () => { + const remainingRequests = await limiter.removeTokens(1) + if (remainingRequests < 0) { + throw new GitHubAPILimitReachedError() + } +} + +/** + * Get the SHA of a file using the GitHub API. + */ +const getFileSHA = async (params: GitHubGetFileParams) => { + try { + await removeToken() + const { data } = await octokit.rest.repos.getContent(params) + + if (Array.isArray(data)) { + const [item] = data + return item.sha + } + + return data.sha + } catch (err) { + if (err instanceof RequestError && err.status === 404) { + return null + } + + throw err + } +} + +/** + * Upload a file using the GitHub API. + */ +export const uploadFile = async (params: GitHubFileUploadParams) => { + const { repo, owner, path } = params + const sha = params.sha ?? await getFileSHA({ repo, owner, path }) + + await removeToken() + return await octokit.rest.repos.createOrUpdateFileContents({ + committer: GITHUB_COMMITTER, + ...params, + ...(sha ? { sha } : {}) + }) +} \ No newline at end of file diff --git a/app/api/github/types.ts b/app/api/github/types.ts new file mode 100644 index 0000000..30a7de7 --- /dev/null +++ b/app/api/github/types.ts @@ -0,0 +1,6 @@ +import { Endpoints } from '@octokit/types' + +export type GitHubGetFileParams = Endpoints["GET /repos/{owner}/{repo}/contents/{path}"]["parameters"] + +export type GitHubFileUploadParams = Endpoints["PUT /repos/{owner}/{repo}/contents/{path}"]["parameters"] +export type GitHubFileUploadResponse = Endpoints["PUT /repos/{owner}/{repo}/contents/{path}"]["response"] \ No newline at end of file diff --git a/app/api/image/image.service.ts b/app/api/image/image.service.ts new file mode 100644 index 0000000..424032d --- /dev/null +++ b/app/api/image/image.service.ts @@ -0,0 +1,72 @@ +import path from "path" +import { uploadFile } from "../github/github.service" +import { ReleasePayload } from "../artist/types" +import createSlug from "@/app/utils/createSlug" +import { ImageUploadOptions, ImageUploadResponse } from "./types" +import { getBlobSHA } from "@/app/utils/hash" + +/** + * The repository which the images will be uploaded. + */ +const IMAGE_REPOSITORY = { + repo: 'images', + owner: 'geracao666', + branch: 'main', +} + +const getArtistDir = (artistSlug: string) => { + const firstChar = artistSlug.charAt(0) + const dir = /[a-z]/.test(firstChar) ? firstChar : '0-9' + + return path.join(dir, artistSlug) +} + +const getCoverPath = (artistSlug: string) => { + const dir = getArtistDir(artistSlug) + return path.join(dir, 'cover.jpg') +} + +const getArtworkPath = (artistSlug: string, release: ReleasePayload) => { + const dir = getArtistDir(artistSlug) + const artworkSlug = createSlug(release.name) + + return path.join(dir, 'artworks', `${artworkSlug}.jpg`) +} + +export const uploadCoverImage = async ( + artistName: string, + artistSlug: string, + coverBase64: string, + { shaFromContent = false }: ImageUploadOptions = {} +): Promise => { + const coverPath = getCoverPath(artistSlug) + const sha = shaFromContent && getBlobSHA(coverBase64) + const commit = { + ...IMAGE_REPOSITORY, + ...(sha ? { sha } : {}), + path: coverPath, + content: coverBase64, + message: `:framed_picture: Upload "${artistName}" cover image.`, + } + + return [coverPath, await uploadFile(commit)] +} + +export const uploadArtworkImage = async ( + artistName: string, + artistSlug: string, + release: ReleasePayload, + { shaFromContent = false }: ImageUploadOptions = {} +): Promise => { + const artworkPath = getArtworkPath(artistSlug, release) + const sha = shaFromContent && getBlobSHA(release.artwork) + const commit = { + ...IMAGE_REPOSITORY, + ...(sha ? { sha } : {}), + path: artworkPath, + content: release.artwork, + message: `:art: Upload ${artistName}'s "${release.name}" artwork image.`, + } + + return [artworkPath, await uploadFile(commit)] +} \ No newline at end of file diff --git a/app/api/image/types.ts b/app/api/image/types.ts new file mode 100644 index 0000000..aece95b --- /dev/null +++ b/app/api/image/types.ts @@ -0,0 +1,7 @@ +import { GitHubFileUploadResponse } from "../github/types" + +export type ImageUploadOptions = { + shaFromContent?: boolean +} + +export type ImageUploadResponse = [string, GitHubFileUploadResponse | null] \ No newline at end of file diff --git a/app/api/tag/route.tsx b/app/api/tag/route.ts similarity index 85% rename from app/api/tag/route.tsx rename to app/api/tag/route.ts index c8acf45..480bddf 100644 --- a/app/api/tag/route.tsx +++ b/app/api/tag/route.ts @@ -7,7 +7,5 @@ export async function GET() { } catch (err) { // TODO: Handle error correctly console.log('Error while retrieving tags:', err) - } finally { - await prisma.$disconnect() } } \ No newline at end of file diff --git a/app/config/github.ts b/app/config/github.ts new file mode 100644 index 0000000..f0d57af --- /dev/null +++ b/app/config/github.ts @@ -0,0 +1,11 @@ +export const GITHUB_COMMITTER = { + name: process.env.GITHUB_USER_NAME ?? '', + email: process.env.GITHUB_USER_EMAIL ?? '' +} + +export const GITHUB_API_RATE_LIMIT = { + interval: 'hour', + tokensPerInterval: 5000 +} + +export const GITHUB_API_TOKEN = process.env.GITHUB_API_TOKEN || '' \ No newline at end of file diff --git a/app/lib/octokit.ts b/app/lib/octokit.ts new file mode 100644 index 0000000..329a801 --- /dev/null +++ b/app/lib/octokit.ts @@ -0,0 +1,14 @@ +import { Octokit } from 'octokit'; +import { GITHUB_API_TOKEN } from '../config/github'; + +const octokitSingleton = () => new Octokit({ auth: GITHUB_API_TOKEN }) + +declare global { + var octokit: undefined | ReturnType +} + +const octokit = globalThis.octokit ?? octokitSingleton() + +export default octokit + +if (process.env.NODE_ENV !== 'production') globalThis.octokit = octokit \ No newline at end of file diff --git a/app/lib/prisma.ts b/app/lib/prisma.ts index 77d6b4b..1a7d695 100644 --- a/app/lib/prisma.ts +++ b/app/lib/prisma.ts @@ -1,15 +1,12 @@ -import { PrismaClient } from '@prisma/client' +import { Prisma, PrismaClient } from '@prisma/client' -const prismaClientSingleton = () => { - return new PrismaClient() -} +const prismaClientSingleton = () => new PrismaClient() declare global { var prisma: undefined | ReturnType } const prisma = globalThis.prisma ?? prismaClientSingleton() - export default prisma if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma \ No newline at end of file diff --git a/app/utils/createSlug.ts b/app/utils/createSlug.ts new file mode 100644 index 0000000..39cfb4a --- /dev/null +++ b/app/utils/createSlug.ts @@ -0,0 +1,7 @@ +import slugify from "slugify"; + +const createSlug = (str: string) => slugify(str, { + lower: true, + strict: true +}) +export default createSlug \ No newline at end of file diff --git a/app/utils/hash.ts b/app/utils/hash.ts new file mode 100644 index 0000000..3cdfe38 --- /dev/null +++ b/app/utils/hash.ts @@ -0,0 +1,9 @@ +import { hashSync } from "hasha" + +/** + * Get the blob SHA from a base64 string. + */ +export const getBlobSHA = (str: string) => { + const binary = Buffer.from(str, 'base64') + return hashSync([`blob ${binary.length}\0`, binary], { algorithm: 'sha1' }) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c21969b..5435352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,18 @@ "axios-hooks": "^5.0.2", "bytes": "^3.1.2", "classnames": "^2.5.0", + "hasha": "^6.0.0", + "http-status-codes": "^2.3.0", + "limiter": "^2.1.0", "mime-types": "^2.1.35", "next": "14.0.4", + "octokit": "^3.1.2", "react": "^18", "react-daisyui": "^5.0.0", "react-dom": "^18", "react-hook-form": "^7.49.2", "react-image-crop": "^11.0.4", + "slugify": "^1.6.6", "yup": "^1.3.3" }, "devDependencies": { @@ -456,6 +461,337 @@ "node": ">= 8" } }, + "node_modules/@octokit/app": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.0.2.tgz", + "integrity": "sha512-NCSCktSx+XmjuSUVn2dLfqQ9WIYePGP95SDJs4I9cn/0ZkeXcPkaoCLl64Us3dRKL2ozC7hArwze5Eu+/qt1tg==", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz", + "integrity": "sha512-9N7IlBAKEJR3tJgPSubCxIDYGXSdc+2xbkjYpk9nCyqREnH8qEMoMhiEB1WgoA9yTFp91El92XNXAi+AjuKnfw==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "deprecation": "^2.3.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", + "integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/types": "^12.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", + "integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", + "dependencies": { + "@octokit/oauth-methods": "^4.0.0", + "@octokit/request": "^8.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", + "integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", + "dependencies": { + "@octokit/auth-oauth-device": "^6.0.0", + "@octokit/oauth-methods": "^4.0.0", + "@octokit/request": "^8.0.2", + "@octokit/types": "^12.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", + "integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.0.tgz", + "integrity": "sha512-7HcYW5tP7/Z6AETAPU14gp5H5KmCPT3hmJrS/5tO7HIgbwenYmgw4OY9Ma54FDySuxMwD+wsJlxtuGWwuZuItA==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.3.0.tgz", + "integrity": "sha512-c/fjpoHispRvBZuRoTVt/uALg7pXa9RQbXWJiDMk6NDkGNomuAZG7YuYYpZoxeoXv+kVRjIDTsO0e1z0pei+PQ==", + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz", + "integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.2.0.tgz", + "integrity": "sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==", + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.5.0.tgz", + "integrity": "sha512-YJEKcb0KkJlIUNU/zjnZwHEP8AoVh/OoIcP/1IyR4UHxExz7fzpe/a8IG4wBtQi7QDEqiomVLX88S6FpxxAJtg==", + "dependencies": { + "@octokit/openapi-types": "^19.1.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.1.1.tgz", + "integrity": "sha512-h7PyYf4VR9kvmm6SYdkmju5BOQmpJ3Fvf1rNQaEsfs70EOu0vspm/Fzr1j5LnP6UGblSW2kuEKiuEwEf5H3OTw==", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.0.0", + "@octokit/webhooks-types": "7.3.2", + "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.0.0.tgz", + "integrity": "sha512-M8mwmTXp+VeolOS/kfRvsDdW+IO0qJ8kYodM/sAysk093q6ApgmBXwK1ZlUvAwXVrp/YVHp6aArj4auAxUAOFw==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.3.2.tgz", + "integrity": "sha512-JWOoOgtWTFnTSAamPXXyjTY5/apttvNxF+vPBnwdSu5cj5snrd7FO0fyw4+wTXy8fHduq626JjhO+TwCyyA6vA==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -565,6 +901,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "devOptional": true }, + "node_modules/@types/aws-lambda": { + "version": "8.10.133", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.133.tgz", + "integrity": "sha512-sr852MAL/79rjDelXP6ZuJ6GwOvXIRrFAoC8a+w91mZ5XR71CuzSgo1d0+pG1qgfPhjFgaibu7SWaoC5BA7pyQ==" + }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" + }, "node_modules/@types/bytes": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.4.tgz", @@ -577,6 +923,14 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime-types": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", @@ -587,7 +941,6 @@ "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", - "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -787,6 +1140,18 @@ "node": ">=0.4.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1127,6 +1492,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1135,6 +1505,11 @@ "node": ">=8" } }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1188,6 +1563,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1315,6 +1700,14 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1503,6 +1896,11 @@ "node": ">=0.4.0" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1595,6 +1993,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.616", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", @@ -2626,6 +3032,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-6.0.0.tgz", + "integrity": "sha512-MLydoyGp9QJcjlhE5lsLHXYpWayjjWqkavzju2ZWD2tYa1CgmML1K1gWAu22BLFa2eZ0OfvJ/DlfoVjaD54U2Q==", + "dependencies": { + "is-stream": "^3.0.0", + "type-fest": "^4.7.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz", + "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -2637,6 +3069,11 @@ "node": ">= 0.4" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -2671,6 +3108,14 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2951,6 +3396,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -3126,6 +3582,27 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -3141,6 +3618,30 @@ "node": ">=4.0" } }, + "node_modules/just-performance": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/just-performance/-/just-performance-4.3.0.tgz", + "integrity": "sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3189,6 +3690,14 @@ "node": ">=10" } }, + "node_modules/limiter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-2.1.0.tgz", + "integrity": "sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==", + "dependencies": { + "just-performance": "4.3.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3209,12 +3718,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3230,7 +3774,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3315,8 +3858,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -3572,11 +4114,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/octokit": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4170,6 +4731,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -4196,7 +4776,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4289,6 +4868,14 @@ "node": ">=8" } }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4877,8 +5464,21 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "devOptional": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universal-github-app-jwt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", + "integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -5122,14 +5722,12 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.3.4", diff --git a/package.json b/package.json index 6021053..f3a7e3c 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,18 @@ "axios-hooks": "^5.0.2", "bytes": "^3.1.2", "classnames": "^2.5.0", + "hasha": "^6.0.0", + "http-status-codes": "^2.3.0", + "limiter": "^2.1.0", "mime-types": "^2.1.35", "next": "14.0.4", + "octokit": "^3.1.2", "react": "^18", "react-daisyui": "^5.0.0", "react-dom": "^18", "react-hook-form": "^7.49.2", "react-image-crop": "^11.0.4", + "slugify": "^1.6.6", "yup": "^1.3.3" }, "devDependencies": { diff --git a/prisma/migrations/20240216101439_make_artist_name_unique/migration.sql b/prisma/migrations/20240216101439_make_artist_name_unique/migration.sql new file mode 100644 index 0000000..63a4f4c --- /dev/null +++ b/prisma/migrations/20240216101439_make_artist_name_unique/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name]` on the table `Artist` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Artist_name_key" ON "Artist"("name"); diff --git a/prisma/migrations/20240217073616_remove_cover_column/migration.sql b/prisma/migrations/20240217073616_remove_cover_column/migration.sql new file mode 100644 index 0000000..b45558c --- /dev/null +++ b/prisma/migrations/20240217073616_remove_cover_column/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `cover` on the `Artist` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Artist" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "origin" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Artist" ("createdAt", "id", "name", "origin", "updatedAt") SELECT "createdAt", "id", "name", "origin", "updatedAt" FROM "Artist"; +DROP TABLE "Artist"; +ALTER TABLE "new_Artist" RENAME TO "Artist"; +CREATE UNIQUE INDEX "Artist_name_key" ON "Artist"("name"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20240217082748_add_cover_column/migration.sql b/prisma/migrations/20240217082748_add_cover_column/migration.sql new file mode 100644 index 0000000..263c65b --- /dev/null +++ b/prisma/migrations/20240217082748_add_cover_column/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Added the required column `cover` to the `Artist` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Artist" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "origin" TEXT NOT NULL, + "cover" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Artist" ("createdAt", "id", "name", "origin", "updatedAt") SELECT "createdAt", "id", "name", "origin", "updatedAt" FROM "Artist"; +DROP TABLE "Artist"; +ALTER TABLE "new_Artist" RENAME TO "Artist"; +CREATE UNIQUE INDEX "Artist_name_key" ON "Artist"("name"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20240217090128_add_slug_column/migration.sql b/prisma/migrations/20240217090128_add_slug_column/migration.sql new file mode 100644 index 0000000..1301f75 --- /dev/null +++ b/prisma/migrations/20240217090128_add_slug_column/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - Added the required column `slug` to the `Artist` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Artist" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "origin" TEXT NOT NULL, + "cover" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Artist" ("cover", "createdAt", "id", "name", "origin", "updatedAt") SELECT "cover", "createdAt", "id", "name", "origin", "updatedAt" FROM "Artist"; +DROP TABLE "Artist"; +ALTER TABLE "new_Artist" RENAME TO "Artist"; +CREATE UNIQUE INDEX "Artist_name_key" ON "Artist"("name"); +CREATE UNIQUE INDEX "Artist_slug_key" ON "Artist"("slug"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20240220091408_change_artwork_column_type/migration.sql b/prisma/migrations/20240220091408_change_artwork_column_type/migration.sql new file mode 100644 index 0000000..a1b2a9f --- /dev/null +++ b/prisma/migrations/20240220091408_change_artwork_column_type/migration.sql @@ -0,0 +1,17 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Release" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "artwork" TEXT NOT NULL, + "downloadUrl" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "releaseTypeId" INTEGER NOT NULL, + CONSTRAINT "Release_releaseTypeId_fkey" FOREIGN KEY ("releaseTypeId") REFERENCES "ReleaseType" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Release" ("artwork", "createdAt", "downloadUrl", "id", "name", "releaseTypeId", "updatedAt") SELECT "artwork", "createdAt", "downloadUrl", "id", "name", "releaseTypeId", "updatedAt" FROM "Release"; +DROP TABLE "Release"; +ALTER TABLE "new_Release" RENAME TO "Release"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20240220092119_add_number_column_to_track_table/migration.sql b/prisma/migrations/20240220092119_add_number_column_to_track_table/migration.sql new file mode 100644 index 0000000..d12c5bb --- /dev/null +++ b/prisma/migrations/20240220092119_add_number_column_to_track_table/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - Added the required column `number` to the `Track` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Track" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "number" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "discId" INTEGER NOT NULL, + CONSTRAINT "Track_discId_fkey" FOREIGN KEY ("discId") REFERENCES "Disc" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Track" ("discId", "id", "name") SELECT "discId", "id", "name" FROM "Track"; +DROP TABLE "Track"; +ALTER TABLE "new_Track" RENAME TO "Track"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b491a11..4cdfb6f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,9 +12,10 @@ datasource db { model Artist { id Int @id @default(autoincrement()) - name String + name String @unique + slug String @unique origin String - cover Bytes + cover String tags Tag[] releases Release[] createdAt DateTime @default(now()) @@ -31,8 +32,8 @@ model Release { id Int @id @default(autoincrement()) type ReleaseType @relation(fields: [releaseTypeId], references: [id]) name String - artwork Bytes - downloadUrl String + artwork String + downloadUrl String // TODO: make optional discs Disc[] artists Artist[] createdAt DateTime @default(now()) @@ -56,6 +57,7 @@ model Disc { model Track { id Int @id @default(autoincrement()) + number Int name String disc Disc @relation(fields: [discId], references: [id]) discId Int diff --git a/prisma/seed.ts b/prisma/seed.ts index c92ab74..bc1a6db 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -26,8 +26,6 @@ async function main() { } } catch (err) { console.log('Error while seeding:', err) - } finally { - await prisma.$disconnect() } }