Set up sequelize

This commit is contained in:
Jorge Vargas 2024-08-19 00:28:48 -06:00
parent 6833439a4a
commit 87c08df02b
47 changed files with 2171 additions and 6 deletions

2
src/constants/index.ts Normal file
View file

@ -0,0 +1,2 @@
export const PLACEHOLDER =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVQImWN4fGrVhZ0z/v+5zZAc5yfOwGCtrsbg4em/f7ZvZ7w2Q15Vi6e1iggPAwBwDg7L//0+xAAAAABJRU5ErkJggg=='

View file

@ -1,19 +1,27 @@
import { makeExecutableSchema } from "@graphql-tools/schema";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import ApolloPackage from '@apollo/client'
const { ApolloClient, InMemoryCache } = ApolloPackage;
import { SchemaLink } from "@apollo/client/link/schema"
import { makeExecutableSchema } from "@graphql-tools/schema";
import { loadFilesSync } from '@graphql-tools/load-files'
import { mergeResolvers } from '@graphql-tools/merge'
import path from "node:path"
import { typeDefs } from "./__generated__/typeDefs.generated";
import { resolvers } from "./__generated__/resolvers.generated";
// import { resolvers } from "./__generated__/resolvers.generated";
import db from "@/sequelize";
import resolverArray from '@/graphql/resolvers'
export const resolvers = mergeResolvers(resolverArray)
const schema = makeExecutableSchema({ typeDefs, resolvers })
export type ResolverContext = { request?: Request; /*session?: Session */ }
export type ResolverContext = { request?: Request, db: any /*session?: Session */ }
export async function getApolloClient(request?: Request) {
// const session = request ? await getSession(request) : undefined
return new ApolloClient({
ssrMode: true,
link: new SchemaLink({ schema, context: { request } }),
link: new SchemaLink({ schema, context: { request, db } }),
cache: new InMemoryCache()
})
}

View file

@ -0,0 +1,7 @@
// import mutations from './mutations'
import queries from './queries'
// import types from './types'
const resolvers = { /*...mutations,*/ ...queries /*...types*/ }
export default resolvers

View file

@ -0,0 +1,54 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
// import axios from 'axios'
import { isAuthedApp } from '@/server/utils/resolvers'
import { getSession, getUser } from '@/next/utils/getSession'
// const token = process.env.IRONCLAD
const resolversComposition = {
'Mutation.*': [isAuthedApp]
}
const resolvers = {
Mutation: {
updateComment: async (_, { text, anon, albumId }, { db }) => {
const { username } = await getSession()
const row = await db.models.comment.findOne({
where: { albumId, username }
})
if (row) {
await row.update({ text, anon })
await row.save()
} else await db.models.comment.create({ albumId, username, text, anon })
return true
},
addFavorite: async (_, { albumId }, { db }) => {
const user = await getUser(db)
await user.addAlbum(albumId)
return true
},
removeFavorite: async (_, { albumId }, { db }) => {
const user = await getUser(db)
await user.removeAlbum(albumId)
return true
},
rateAlbum: async (_, { albumId, score }, { db }) => {
const { username } = await getSession()
const row = await db.models.rating.findOne({
where: { albumId, username }
})
if (row) {
await row.update({ score })
await row.save()
} else await db.models.rating.create({ albumId, username, score })
return true
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,81 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { createLog, createUpdateLog } from '@/server/utils/log'
import { getImgColor, img } from '@/server/utils/image'
import { hasRole } from '@/server/utils/resolvers'
import { handleComplete } from '@/server/utils/requests'
import { slugify } from '@/server/utils/slugify'
import { UserInputError } from '@/next/server/utils/graphQLErrors'
const resolversComposition = { 'Mutation.*': hasRole('CREATE') }
const resolvers = {
Mutation: {
createAlbum: async (parent, data, { db }, info) =>
db.transaction(async (transaction) => {
data.artists = data.artists
? data.artists.map((artist) => {
return { name: artist, slug: slugify(artist) }
})
: []
await db.models.artist.bulkCreate(data.artists, {
ignoreDuplicates: true,
transaction
})
const album = await db.models.album.create(data, {
include: [
db.models.disc,
db.models.store,
{
model: db.models.download,
include: [db.models.link]
}
],
transaction
})
await Promise.all([
album.setArtists(
data.artists
.filter(({ slug }) => slug.length > 0)
.map(({ slug }) => slug),
{ transaction }
),
album.setCategories(data.categories || [], { transaction }),
album.setClassifications(data.classifications || [], { transaction }),
album.setPlatforms(data.platforms || [], { transaction }),
album.setGames(data.games || [], { transaction }),
album.setAnimations(data.animations || [], { transaction }),
album.setRelated(data.related || [], { transaction }),
createLog(db, 'createAlbum', data, transaction)
])
const { id } = album.dataValues
album.placeholder = data.cover
? await img(data.cover, 'album', id)
: undefined
album.headerColor = data.cover
? await getImgColor(`album/${id}`)
: undefined
await album.save({ transaction })
if (album.status === 'show') handleComplete(db, data, album)
return album
}),
deleteAlbum: async (parent, { id }, { db }, info) => {
const album = await db.models.album.findByPk(id)
if (!album) throw UserInputError('Not Found')
return db.transaction(async (transaction) => {
await createUpdateLog(db, 'deleteAlbum', album, transaction)
await album.destroy({ transaction })
return 1
})
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,12 @@
import merge from 'lodash/merge'
import comments from './comments'
import create from './create'
import requests from './requests'
import site from './site'
import update from './update'
import user from './user'
const mutations = merge(comments, create, requests, site, update, user)
export default mutations

View file

@ -0,0 +1,86 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { mergeResolvers } from '@graphql-tools/merge'
import { hasRole, isAuthedApp } from '@/server/utils/resolvers'
import { getUser } from '@/next/utils/getSession'
import { requestPOST } from '@/server/utils/requests'
import { UserInputError } from '@/next/server/utils/graphQLErrors'
const resolvers = {
Mutation: {
editRequest: async (parent, data, { db }, info) => {
const request = await db.models.request.findByPk(data.id)
if (!request) throw UserInputError('Request not found')
await db.transaction(async (transaction) => {
await request.set(data, { transaction })
if (request.changed('state')) {
switch (request.state) {
case 'complete':
await requestPOST('complete', { requestId: request.id })
break
case 'hold':
await requestPOST('hold', {
requestId: request.id,
reason: data.reason
})
break
}
}
await request.save({ transaction })
})
return request
},
rejectRequest: async (parent, data, { db }, info) => {
const request = await db.models.request.findByPk(data.id)
if (!request) throw UserInputError('Request not found')
await requestPOST('reject', {
requestId: request.id,
reason: data.reason
})
return true
}
}
}
const submitActions = {
Mutation: {
submitAlbum: async (parent, data, { db }, info) => {
const { request: requestId, title, vgmdb, links } = data
let request
if (requestId) {
request = await db.models.request.findByPk(requestId)
if (!request) throw UserInputError('Request not found')
if (request.state === 'complete')
throw UserInputError('Request already complete')
}
const user = await getUser(db)
return db.models.submission.create({
title,
vgmdb,
links,
requestId,
userUsername: user.username
})
}
}
}
const requestResolvers = composeResolvers(resolvers, {
'Mutation.*': hasRole('REQUESTS')
})
const submitResolvers = composeResolvers(submitActions, {
'Mutation.*': [isAuthedApp]
})
export default mergeResolvers([requestResolvers, submitResolvers])

View file

@ -0,0 +1,37 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import fs from 'fs-extra'
import path from 'path'
import { img } from '@/server/utils/image'
import { hasRole } from '@/server/utils/resolvers'
import { UserInputError } from '@/next/server/utils/graphQLErrors'
const resolversComposition = { 'Mutation.*': hasRole('UPDATE') }
const resolvers = {
Mutation: {
config: async (parent, data, { db, payload }, info) =>
db.models.config
.upsert(data)
.then(() => db.models.config.findByPk(data.name)),
uploadBanner: async (parent, { banner }, { db, payload }) => {
const timestamp = Date.now()
await img(banner, 'live', timestamp)
await db.models.config.upsert({ name: 'banner', value: timestamp })
return 1
},
selectBanner: async (parent, { name }, { db }) => {
const filePath = path.join('/var/www/soc_img/img/live', `${name}.png`)
if (!(await fs.pathExists(filePath)))
throw UserInputError(`Banner '${name}' doesnt exist`)
await db.models.config.upsert({ name: 'banner', value: name })
return 1
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,335 @@
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { createLog, createUpdateLog } from '@/server/utils/log'
import { img, getImgColor } from '@/server/utils/image'
import { hasRole } from '@/server/utils/resolvers'
import { handleComplete } from '@/server/utils/requests'
import { slugify } from '@/server/utils/slugify'
const resolversComposition = { 'Mutation.*': hasRole('UPDATE') }
const resolvers = {
Mutation: {
createPublisher: async (parent, data, { db }, info) =>
db.transaction(async (transaction) => {
const pub = await db.models.publisher.create(data, { transaction })
data.id = pub.id
await createLog(db, 'createPublisher', data, transaction)
return pub
}),
updatePublisher: async (parent, { id, name }, { db }, info) => {
const pub = await db.models.publisher.findByPk(id)
pub.name = name
return db.transaction(async (transaction) => {
await pub.save({ transaction })
await createUpdateLog(db, 'updatePublisher', pub, transaction)
return pub
})
},
deletePublisher: async (parent, { id }, { db }) => {
const pub = await db.models.publisher.findByPk(id)
return db.transaction(async (transaction) => {
await pub.destroy({ transaction })
await createLog(db, 'deletePublisher', pub.dataValues, transaction)
})
},
createPlatform: async (parent, data, { db }, info) =>
db.transaction(async (transaction) => {
const plat = db.models.platform.create(data, { transaction })
data.id = plat.id
await createLog(db, 'createPlatform', data, transaction)
return plat
}),
updatePlatform: async (parent, { key, name, type }, { db }, info) => {
const plat = await db.models.platform.findByPk(key)
if (name) plat.name = name
if (type !== plat.type) plat.type = type
return db.transaction(async (transaction) => {
await plat.save({ transaction })
await createUpdateLog(db, 'updatePlatform', plat, transaction)
return plat
})
},
deletePlatform: async (parent, { key }, { db }) => {
const plat = await db.models.platform.findByPk(key)
return db.transaction(async (transaction) => {
await plat.destroy({ transaction })
await createLog(db, 'deletePlatform', plat.dataValues, transaction)
})
},
createStudio: async (parent, data, { db }, info) =>
db.transaction(async (transaction) => {
const studio = db.models.studio.create(data, { transaction })
data.slug = studio.slug
await createLog(db, 'createStudio', data, transaction)
return studio
}),
updateStudio: async (parent, { slug, name }, { db }, info) => {
const studio = await db.models.studio.findByPk(slug)
studio.name = name
return db.transaction(async (transaction) => {
await studio.save({ transaction })
await createUpdateLog(db, 'updateStudio', studio, transaction)
return studio
})
},
deleteStudio: async (parent, { slug, name }, { db }, info) => {
const studio = await db.models.studio.findByPk(slug)
return db.transaction(async (transaction) => {
studio.destroy({ transaction })
await createLog(db, 'deleteStudio', studio.dataValues, transaction)
})
},
createSeries: async (parent, data, { db }, info) =>
db.transaction(async (transaction) => {
const series = await db.models.series.create(data, { transaction })
const { slug } = series.dataValues
series.placeholder = data.cover
? await img(data.cover, 'series', slug)
: undefined
series.headerColor = data.cover
? await getImgColor(`series/${slug}`)
: undefined
await series.save({ transaction })
await createLog(db, 'createSeries', data, transaction)
return series
}),
updateSeries: async (parent, { slug, name, cover }, { db }, info) => {
const series = await db.models.series.findByPk(slug)
if (name) series.name = name
if (cover) {
series.placeholder = await img(cover, 'series', slug)
series.headerColor = await getImgColor(`series/${slug}`)
}
return db.transaction(async (transaction) => {
await series.save({ transaction })
await createUpdateLog(db, 'updateSeries', series, transaction)
return series
})
},
deleteSeries: async (parent, { slug }, { db }) => {
const series = await db.models.series.findByPk(slug)
return db.transaction(async (transaction) => {
await series.destroy({ transaction })
await createLog(db, 'deleteSeries', series.dataValues, transaction)
})
},
createGame: async (parent, data, { db }, info) => {
const game = await db.models.game.create(data)
return db.transaction(async (transaction) => {
await Promise.all([
game.setSeries(data.series, { transaction }),
game.setPublishers(data.publishers, { transaction }),
game.setPlatforms(data.platforms, { transaction })
])
game.placeholder = data.cover
? await img(data.cover, 'game', data.slug)
: ''
game.headerColor = data.cover
? await getImgColor(`game/${data.slug}`)
: undefined
await game.save({ transaction })
await createLog(db, 'createGame', data, transaction)
return game
})
},
updateGame: async (parent, args, { db }, info) => {
const {
slug,
name,
cover,
releaseDate,
series = [],
publishers,
platforms
} = args
const game = await db.models.game.findByPk(slug)
game.name = name
game.releaseDate = releaseDate
if (cover) {
game.placeholder = await img(cover, 'game', slug)
series.headerColor = await getImgColor(`game/${slug}`)
}
// make more comprehensible log
return db.transaction(async (transaction) => {
game.setSeries(series, { transaction })
game.setPublishers(publishers, { transaction })
game.setPlatforms(platforms, { transaction })
await game.save({ transaction })
await createUpdateLog(db, 'updateGame', game, transaction)
return game
})
},
deleteGame: async (parent, { slug }, { db }) => {
const game = await db.models.game.findByPk(slug)
const log = {
...game.dataValues,
series: await game.getSeries(),
publishers: await game.getPublishers(),
platforms: await game.getPlatforms()
}
return db.transaction(async (transaction) => {
await game.destroy({ transaction })
await createLog(db, 'deleteSeries', log, transaction)
})
},
createAnimation: async (parent, data, { db }, info) => {
return db.transaction(async (transaction) => {
const anim = await db.models.animation.create(data, { transaction })
await anim.setStudios(data.studios, { transaction })
anim.placeholder = data.cover
? await img(data.cover, 'anim', anim.id)
: ''
anim.headerColor = data.cover
? await getImgColor(`anim/${anim.id}`)
: undefined
await anim.save({ transaction })
await createLog(db, 'createAnimation', data, transaction)
return anim
})
},
updateAnimation: async (parent, data, { db }, info) => {
const anim = await db.models.animation.findByPk(data.id)
Object.entries(data).forEach(([key, value]) => {
anim[key] = value
})
if (data.cover) {
anim.placeholder = await img(data.cover, 'anim', anim.id)
anim.headerColor = await getImgColor(`anim/${anim.id}`)
}
return db.transaction(async (transaction) => {
anim.setStudios(data.studios, { transaction })
await anim.save({ transaction })
await createUpdateLog(db, 'updateAnimation', anim, transaction)
return anim
})
},
deleteAnimation: async (parent, { id }, { db }) => {
const anim = await db.models.animation.findByPk(id)
const log = {
...anim.dataValues,
studios: await anim.getStudios()
}
return db.transaction(async (transaction) => {
await anim.destroy({ transaction })
await createLog(db, 'deleteAnim', log, transaction)
})
},
updateAlbum: async (parent, data, { db }, info) => {
try {
const album = await db.models.album.findByPk(data.id)
const triggerPost =
data.status !== album.status.repeat(1) && data.status === 'show'
data.artists = data.artists
? data.artists.map((artist) => {
return { name: artist, slug: slugify(artist) }
})
: []
await db.transaction(async (transaction) => {
await db.models.artist.bulkCreate(data.artists, {
ignoreDuplicates: true,
transaction
})
// implement better log lol lmao
await Promise.all([
album.update(data, { transaction }),
album.setArtists(
data.artists.map(({ slug }) => slug),
{ transaction }
),
album.setCategories(data.categories || [], { transaction }),
album.setClassifications(data.classifications || [], {
transaction
}),
album.setPlatforms(data.platforms || [], { transaction }),
album.setGames(data.games || []),
{ transaction },
album.setRelated(data.related || [], { transaction }),
album.setAnimations(data.animations || [], { transaction }),
db.models.disc
.destroy({ where: { albumId: album.dataValues.id }, transaction })
.then(() =>
(data.discs || []).map((disc) =>
album.createDisc(disc, { transaction })
)
),
db.models.store
.destroy({ where: { albumId: album.dataValues.id }, transaction })
.then(() =>
(data.stores || []).map((store) =>
album.createStore(store, { transaction })
)
),
db.models.download
.destroy({ where: { albumId: album.dataValues.id }, transaction })
.then(() =>
(data.downloads || []).map((download) =>
album.createDownload(download, {
include: [db.models.link],
transaction
})
)
),
createUpdateLog(db, 'updateAlbum', album, transaction)
])
if (data.cover) {
album.placeholder = await img(data.cover, 'album', album.id)
album.headerColor = await getImgColor(`album/${album.id}`)
await album.save({ transaction })
}
})
if (triggerPost) handleComplete(db, data, album)
return album
} catch (err) {
console.log(err)
throw new Error(err.message)
}
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,220 @@
import bcrypt from 'bcrypt'
import generator from 'generate-password'
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { DateTime } from 'luxon'
import { Op } from 'sequelize'
import path from 'path'
import fs from 'fs-extra'
import sharp from 'sharp'
import { createForgor } from '@/server/utils/forgor'
import { isAuthedApp } from '@/server/utils/resolvers'
import { processImage } from '@/server/utils/image'
import { getSession, getUser } from '@/next/utils/getSession'
import {
ForbiddenError,
UserInputError
} from '@/next/server/utils/graphQLErrors'
const resolversComposition = {
'Mutation.updateUser': [isAuthedApp]
}
const streamToString = (stream) => {
const chunks = []
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('error', (err) => reject(err))
stream.on('end', () => resolve(Buffer.concat(chunks)))
})
}
async function cropPFP(streamItem, username, imgId) {
const { createReadStream } = await streamItem
const pathString = '/var/www/soc_img/img/user'
const fullPath = path.join(pathString, `${username}_${imgId}.png`)
await fs.ensureDir(pathString)
const image = await streamToString(createReadStream())
let sharpImage = sharp(image)
const metadata = await sharpImage.metadata()
const { width, height } = metadata
if (width !== height) {
sharpImage = sharpImage.extract(
width > height
? {
left: Math.floor((width - height) / 2),
top: 0,
width: height,
height
}
: {
left: 0,
top: Math.floor((height - width) / 2),
width,
height: width
}
)
}
await sharpImage.resize({ width: 200, height: 200 }).png().toFile(fullPath)
return await processImage(fullPath)
}
const resolvers = {
Mutation: {
login: async (_, { username, password }, { db }) => {
const user = await db.models.user.findByPk(username)
if (!user) throw UserInputError()
const valid = await bcrypt.compare(password, user.password)
if (!valid) throw UserInputError()
const session = await getSession()
session.username = user.username
// Remove this when new site version is fully implemented
session.permissions = (await user.getRoles())
.map((r) => r.permissions)
.flat()
await session.save()
return 200
},
logout: async () => {
const session = await getSession()
await session.destroy()
return 200
},
registerUser: async (_, { username, email, pfp }, { db }) => {
await Promise.all([
db.models.user.findByPk(username).then((result) => {
if (result) throw UserInputError('Username already in use')
}),
db.models.user.findOne({ where: { email } }).then((result) => {
if (result) throw UserInputError('Email already in use')
})
])
const password = generator.generate({
length: 30,
numbers: true,
upercase: true,
strict: true
})
return db.transaction(async (transaction) => {
const user = await db.models.user.create(
{ username, email, password: await bcrypt.hash(password, 10) },
{ transaction }
)
if (pfp) {
const imgId = Date.now()
user.placeholder = await cropPFP(pfp, username, imgId)
user.imgId = imgId
} else {
user.placeholder =
'data:image/webp;base64,UklGRlQAAABXRUJQVlA4IEgAAACwAQCdASoEAAQAAUAmJZgCdAEO9p5AAPa//NFYLcn+a7b+3z7ynq/qXv+iG0yH/y1D9eBf9pqWugq9G0RnxmxwsjaA2bW8AAA='
}
await user.save({ transaction })
await createForgor(user, db, transaction)
return true
})
},
updateUserRoles: async (
parent,
{ username, roles },
{ db, payload },
info
) => {
const user = await db.models.user.findByPk(username)
user.setRoles(roles)
await user.save()
return true
},
deleteUser: async (parent, { username }, { db, payload }, info) => {
const user = await db.models.user.findByPk(username)
if (!user) throw UserInputError('Not Found')
user.destroy()
return 1
},
createForgorLink: async (_, { key }, { db }) => {
const user = await db.models.user.findOne({
where: { [Op.or]: [{ username: key }, { email: key }] }
})
if (!user) throw UserInputError('Not Found')
await createForgor(user, db)
return true
},
updatePass: async (_, { key, pass }, { db }) => {
const row = await db.models.forgor.findByPk(key)
if (!row) throw ForbiddenError()
const now = DateTime.now()
const expires = DateTime.fromJSDate(row.expires)
if (now > expires) throw ForbiddenError()
const user = await db.models.user.findByPk(row.username)
user.password = await bcrypt.hash(pass, 10)
return db.transaction(async (transaction) => {
await user.save({ transaction })
await row.destroy({ transaction })
return true
})
},
updateUser: async (_, { username, email, password, pfp }, { db }) => {
const user = await getUser(db)
if (username) user.username = username
if (email) user.email = email
if (password) user.password = await bcrypt.hash(password, 10)
if (pfp) {
const pathString = '/var/www/soc_img/img/user'
await fs.remove(
path.join(pathString, `${user.username}_${user.imgId}.png`)
)
const imgId = Date.now()
user.placeholder = await cropPFP(pfp, username, imgId)
user.imgId = imgId
}
await user.save()
return true
},
createRole: async (parent, args, { db, payload }) =>
db.models.role.create(args),
updateRole: async (parent, { key, name, permissions }, { db, payload }) => {
const role = await db.models.role.findByPk(key)
if (!role) throw UserInputError('Not Found')
if (role.name !== name) {
await db.query(
`UPDATE roles SET name = "${name}" WHERE name = "${key}"`
)
}
role.permissions = permissions
await role.save()
return role
},
deleteRole: async (parent, { name }, { db, payload }) => {
const role = await db.models.role.findByPk(name)
if (!role) throw UserInputError('Not Found')
await role.destroy()
return name
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,58 @@
import { Op } from 'sequelize'
const resolvers = {
Query: {
artists: (parent, args, { db }, info) => db.models.artist.findAll(),
platforms: (parent, args, { db }, info) => db.models.platform.findAll(),
publishers: (parent, args, { db }, info) => db.models.publisher.findAll(),
publisher: (parent, { id }, { db }, info) =>
db.models.publisher.findByPk(id),
categories: (parent, args, { db }, info) => db.models.category.findAll(),
classifications: (parent, args, { db }, info) =>
db.models.classification.findAll(),
series: (parent, args, { db }, info) => db.models.series.findAll(),
games: (parent, args, { db }, info) => db.models.game.findAll(),
game: (parent, { slug }, { db }, info) => db.models.game.findByPk(slug),
album: (_, { id }, { db }) => db.models.album.findByPk(id),
downloads: (parent, { id }, { db }) =>
db.models.download.findAll({ where: { albumId: id } }),
albums: (_, __, { db }, info) =>
db.models.album.findAll({
}),
platform: async (parent, { id }, { db }) => db.models.platform.findByPk(id),
animation: (parent, { id }, { db }) => db.models.animation.findByPk(id),
animations: (parent, args, { db }) => db.models.animation.findAll(),
studio: (parent, { slug }, { db }) => db.models.studio.findByPk(slug),
studios: (parent, { slug }, { db }) => db.models.studio.findAll(),
seriesOne: (parent, { slug }, { db }, info) =>
db.models.series.findByPk(slug),
albumCount: async (parent, params, { db }) => db.models.album.count(),
recentSeries: (parent, { limit }, { db }) =>
db.models.series.findAll({
limit,
order: [['createdAt', 'DESC']]
}),
recentPublishers: (parent, { limit }, { db }) =>
db.models.publisher.findAll({
limit,
order: [['createdAt', 'DESC']]
}),
recentPlatforms: (parent, { limit, type }, { db }) =>
db.models.platform.findAll({
limit,
order: [['createdAt', 'DESC']],
where: { type: { [Op.like]: `%${type}%` } }
}),
getRandomAlbum: async (parent, { limit = 1 }, { db }) => {
const result = await db.models.album.findAll({
order: db.random(),
limit
})
return result
}
}
}
export default resolvers

View file

@ -0,0 +1,15 @@
import merge from 'lodash/merge'
import search from './search'
/*
import album from './album'
import requests from './requests'
import site from './site'
import user from './user'
import vgmdb from './vgmdb'
*/
const queries = merge(search/*album, requests, search, site, user, vgmdb*/)
export default queries

View file

@ -0,0 +1,76 @@
import { Op, fn, col, where } from 'sequelize'
const resolvers = {
Query: {
requests: (_, {
state = ['complete', 'hold', 'pending'],
donator = [true, false]
}, { db }) => db.models.request.findAll({ where: { state, donator } }),
request: (_, { link }, { db }) => db.models.request.findOne({ where: { link } }),
searchRequests: async (_, {
state = ['complete', 'hold', 'pending'],
donator = [true, false],
limit = 10,
page = 0,
filter
}, { db }) => {
const options = { limit, offset: limit * page }
const optionsWhere = { state, donator }
async function exactSearch () {
if (!filter) return
const results = await db.models.request.findAndCountAll({
where: {
...optionsWhere,
[Op.or]: [
{ id: filter },
{ link: filter },
{ user: filter },
{ userID: filter }
]
},
...options
})
if (results.rows.length > 0) return results
}
function looseSearch () {
return db.models.request.findAndCountAll({
where: [
optionsWhere,
where(fn('LOWER', col('title')), { [Op.like]: `%${filter || ''}%` })
],
...options
})
}
return await exactSearch() || looseSearch()
},
submissions: (_, args, context) => {
const { filter = '', state = ['pending'] } = args
const { db } = context
return db.models.submission.findAll({
where: {
[Op.and]: [
{ state: { [Op.in]: state } },
{
[Op.or]: [
{ id: filter },
{ vgmdb: filter },
{ userUsername: filter },
where(fn('LOWER', col('title')), { [Op.like]: `%${filter.toLowerCase()}%` })
]
}
]
}
})
}
}
}
export default resolvers

View file

@ -0,0 +1,82 @@
import Sequelize from 'sequelize'
const { Op, literal } = Sequelize
import type { Resolvers } from '@/graphql/__generated__/types.generated'
const fuzzySearch = (words: string[]) => `^${words.map(w => `(?=.*\b${w}\b)`)}.+/i`
const resolvers: Resolvers = {
Query: {
searchAlbum: (parent, args, { db }) => {
const { title, categories, limit = 10, offset = 0, order = ['createdAt'], mode = 'DESC', status = ['show'] } = args
const titleWords = title?.split(' ') || []
return db.models.album.findAndCountAll({
limit, offset,
where: {
[Op.or]: [
{ title: { [Op.regexp]: fuzzySearch(titleWords) } },
{ subTitle: { [Op.regexp]: fuzzySearch(titleWords) } }
],
status: { [Op.in]: status }
},
include: categories ? [{ model: db.models.category, where: { name: { [Op.in]: categories } } }] : [],
order: [literal('`album`.`status` = \'coming\' DESC'), ...order.map(o => [o, mode])]
})
},
/* searchAlbumByArtist: async (parent, { name, categories, limit, page = 0, order = ['createdAt'], mode = 'DESC', status = ['show'] }, { db }) => {
const include = [{ model: db.models.artist, where: { name: { [Op.like]: `%${name}%` } } }]
if (categories) include.push({ model: db.models.class, where: { name: { [Op.in]: categories } } })
return searchPage({ limit, page, model: 'album' }, {
where: { status: { [Op.in]: status } },
include,
order: order.map(o => [o, mode])
}, db)
},
searchAnimation: (parent, { title = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ title, limit, page, model: 'animation' }, {
where: { title: { [Op.like]: `%${title}%` } },
order: [[order, mode]]
}, db),
searchStudio: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'studio' }, {
where: { name: { [Op.like]: `%${name}%` } },
order: [[order, mode]]
}, db),
searchGame: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'game' }, {
where: { name: { [Op.like]: `%${name}%` } },
order: [[order, mode]]
}, db),
searchSeries: (parent, { name = '', limit, page = 0, order = 'createdAt', mode = 'DESC' }, { db }) => searchPage({ name, limit, page, model: 'series' }, {
where: { name: { [Op.like]: `%${name}%` } },
order: [[order, mode]]
}, db),
searchSeriesByName: (parent, { name }, { db }) => db.models.series.findAll({
where: {
name: {
[Op.like]: `%${name}%`
}
}
}),
searchPublishersByName: (parent, { name }, { db }) => db.models.publisher.findAll({
where: {
name: {
[Op.like]: `%${name}%`
}
}
}),
searchPlatformsByName: (parent, { name, categories }, { db }) => db.models.platform.findAll({
where: {
name: {
[Op.like]: `%${name}%`
},
type: { [Op.or]: categories }
}
}),
searchPlatformsByCategories: (parent, { categories }, { db }) => categories.length === 0
? []
: db.models.platform.findAll({ where: { type: { [Op.or]: categories } } }) */
}
}
export default resolvers

View file

@ -0,0 +1,36 @@
import fg from 'fast-glob'
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import { hasRole } from '@/server/utils/resolvers'
const resolversComposition = { 'Query.banners': hasRole('UPDATE') }
const resolvers = {
Query: {
config: (parent, { name }, { db }, info) => {
return db.models.config
.findOrCreate({ where: { name } })
.then(() => db.models.config.findByPk(name))
},
highlight: async (parent, args, { db }) => {
const { value } = await db.models.config.findByPk('highlight')
return db.models.album.findByPk(value)
},
banners: async (parent, args) => {
const filePaths = await fg(['/var/www/soc_img/img/live/**/*.png'])
const images = filePaths.map((f) => f.split('/').pop())
return images
},
recentComments: async (parent, { limit = 5 }, { db }) => {
return db.models.comment.findAll({
limit,
order: [['updatedAt', 'DESC']]
})
}
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,33 @@
import { Op } from 'sequelize'
import { composeResolvers } from '@graphql-tools/resolvers-composition'
import info from '@/next/constants/info.json'
import { hasRole } from '@/server/utils/resolvers'
import { getUser } from '@/next/utils/getSession'
const { permissions } = info
const resolversComposition = { 'Query.users': hasRole('MANAGE_USER') }
const resolvers = {
Query: {
me: (parent, args, { db }) => getUser(db),
permissions: () => permissions,
roles: (parent, args, { db }) => db.models.role.findAll(),
users: (parent, args, { db }) => {
const search = args.search.trim()
if (search.length < 3) return []
return db.models.user.findAll({
where: {
[Op.or]: [
{ username: { [Op.like]: `%${search}%` } },
{ email: search }
]
}
})
},
user: async (_, { user }, { db }) => user
}
}
export default composeResolvers(resolvers, resolversComposition)

View file

@ -0,0 +1,9 @@
import getPuppeteer from 'vgmdb-parser/lib/puppeteer'
const resolvers = {
Query: {
vgmdb: (_, { url }) => getPuppeteer(url)
}
}
export default resolvers

View file

@ -0,0 +1,154 @@
import { GraphQLUpload } from 'graphql-upload-minimal'
import {
checkHeaderColor,
solveRating,
checkPlaceholder
} from '@/server/utils/resolvers'
import { getUser } from '@/next/utils/getSession'
const resolvers = {
Upload: GraphQLUpload,
Album: {
artists: (parent, args, context, info) => parent.getArtists(),
categories: (parent, args, context, info) => parent.getCategories(),
classifications: (parent, args, context, info) =>
parent.getClassifications(),
platforms: (parent, args, context, info) =>
parent.getPlatforms({ order: ['name'] }),
games: (parent, args, context, info) => parent.getGames(),
discs: (parent, args, context, info) =>
parent.getDiscs({ order: [['number', 'ASC']] }),
related: (parent, args, context, info) => parent.getRelated(),
stores: (parent) => parent.getStores(),
animations: (parent) => parent.getAnimations(),
downloads: (parent) => parent.getDownloads(),
comments: (parent) => parent.getComments(),
isFavorite: async (album, _, { db }) => {
const user = await getUser(db)
return user ? album.hasUser(user.username) : null
},
selfComment: async (album, _, { db }) => {
const user = await getUser(db)
return user
? db.models.comment.findOne({
where: { albumId: album.id, username: user.username }
})
: null
},
selfScore: async (album, _, { db }) => {
const user = await getUser(db)
return user
? (
await db.models.rating.findOne({
where: { albumId: album.id, username: user.username }
})
)?.score
: null
},
favorites: (album, _, { db }) => album.countUsers(),
placeholder: (album, _, { db }) => checkPlaceholder(album, 'album'),
headerColor: (album, _, { db }) => checkHeaderColor(album, 'album'),
avgRating: async (album, _, { db }) => solveRating(album)
},
Comment: {
username: (parent) => (parent.anon ? null : parent.username),
album: (comment, _, { db }) => comment.getAlbum()
},
Category: {
albums: (parent) => parent.getAlbums(),
count: (parent, args, { db }) =>
db.models.album.count({
include: [{ model: db.models.category, where: { name: parent.name } }]
})
},
Download: {
links: async (download) => {
const links = await download.getLinks()
const filterLinks = links.filter(
(link) => !link.url.includes('adshrink.it')
)
const outLinks = filterLinks.length === 0 ? links : filterLinks
// return outLinks.filter((link) => link.provider !== 'TERABOX')
return outLinks
}
},
Link: {
url: async (link) => {
const download = await link.getDownload()
const links = await download.getLinks()
return links.every((link) => link.url.includes('adshrink.it'))
? link.directUrl
: link.url
},
directUrl: async (link, args, { db }) => {
const download = await link.getDownload()
const links = await download.getLinks()
const fallback = links.every((link) => link.url.includes('adshrink.it'))
if (fallback) return
const user = await getUser(db)
if (!user) return null
const roles = await user.getRoles()
const perms = roles.map((r) => r.permissions).flat()
const donator = perms.includes('DIRECT')
if (!donator) return null
return link.directUrl
}
},
Game: {
albums: async (game, { order = [] }) => game.getAlbums({ order }),
series: (parent, args, context, info) => parent.getSeries(),
publishers: (parent, args, context, info) => parent.getPublishers(),
platforms: (parent, args, context, info) =>
parent.getPlatforms({ order: ['name'] }),
placeholder: (game, _, { db }) => checkPlaceholder(game, 'game'),
headerColor: (game, _, { db }) => checkHeaderColor(game, 'game')
},
Platform: {
albums: (parent) => parent.getAlbums(),
games: (platform, args, { db }) => platform.getGames()
},
Animation: {
studios: (parent) => parent.getStudios(),
albums: (anim, { order = [] }) => anim.getAlbums({ order }),
placeholder: (anim, _, { db }) => checkPlaceholder(anim, 'anim'),
headerColor: (anim, _, { db }) => checkPlaceholder(anim, 'anim')
},
Studio: {
animations: (studio) => studio.getAnimations()
},
Series: {
games: (parent, args, context, info) => parent.getGames(),
placeholder: (series, _, { db }) => checkPlaceholder(series, 'series'),
headerColor: (series, _, { db }) => checkPlaceholder(series, 'series')
},
Publisher: {
games: (parent, args, context, info) => parent.getGames()
},
Disc: {
album: (parent) => parent.getAlbum(),
tracks: (parent) => parent.body.split(',')
}
}
export default resolvers

View file

@ -0,0 +1,8 @@
import merge from 'lodash/merge'
import album from './album'
import user from './user'
const types = merge(album, user)
export default types

View file

@ -0,0 +1,46 @@
import { Op } from 'sequelize'
import pages from '@/next/constants/pages.json'
import { getUser } from '@/next/utils/getSession'
const userResolvable = {
roles: parent => parent.getRoles(),
permissions: async parent => {
const roles = await parent.getRoles()
return roles.map(r => r.permissions).flat()
},
pages: async parent => {
const roles = await parent.getRoles()
const permissions = roles.map(r => r.permissions).flat()
return pages.filter(({ perms }) => perms.length === 0 || perms.some(r => permissions.includes(r)))
},
comments: (user, _, { db }) => user.getComments({ where: { albumId: { [Op.not]: null } } }),
favorites: user => user.getAlbums(),
imgUrl: async user => `https://cdn.sittingonclouds.net/user/${
user.imgId ? `${user.username}_${user.imgId}` : 'default'
}.png`
}
const funcs = {
User: userResolvable,
UserMe: userResolvable,
Role: { permissions: parent => typeof parent.permissions === 'string' || parent.permissions instanceof String ? JSON.parse(parent.permissions) : parent.permissions },
Submission: {
submitter: submission => submission.getUser(),
links: async (submission, _, { db }) => {
const user = await getUser(db)
if (!user) return null
const roles = await user.getRoles()
const perms = roles.map(r => r.permissions).flat()
if (!perms.includes('REQUESTS')) return null
return submission.links
},
request: submission => submission.getRequest()
}
}
export default funcs

View file

@ -145,8 +145,8 @@ type Query {
searchAlbum(
title: String
categories: [String]
offset: Int
limit: Int
page: Int
order: [String]
mode: String
status: [String!]

22
src/sequelize/index.js Normal file
View file

@ -0,0 +1,22 @@
import mysql2 from 'mysql2'
import { Sequelize } from 'sequelize'
import relations from './relations'
import models from './models'
const options = process.env.GITHUB_ACTIONS
? 'sqlite::memory:'
: JSON.parse(import.meta.env.SEQUELIZE)
if (!process.env.GITHUB_ACTIONS && options.dialect === 'mysql')
options.dialectModule = mysql2
if (import.meta.env.DEV && options.logging === undefined)
options.logging = console.log
const db = new Sequelize(options)
Object.values(models).forEach((model) => model(db))
relations(db)
export default db

View file

@ -0,0 +1,22 @@
import { DataTypes } from 'sequelize'
import { PLACEHOLDER } from '@/constants'
const model = (sequelize) =>
sequelize.define('album', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: DataTypes.STRING,
subTitle: DataTypes.TEXT,
releaseDate: DataTypes.DATEONLY,
label: DataTypes.STRING,
vgmdb: DataTypes.STRING,
description: DataTypes.STRING,
status: { type: DataTypes.STRING, defaultValue: 'show' },
placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER },
headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' }
})
export default model

View file

@ -0,0 +1,35 @@
import { DataTypes } from 'sequelize'
import { PLACEHOLDER } from '@/constants'
const animation = (sequelize) => {
sequelize.define(
'animation',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: { type: DataTypes.STRING, unique: true },
subTitle: { type: DataTypes.STRING },
releaseDate: DataTypes.DATEONLY,
placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER },
headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' }
},
{ freezeTableName: true }
)
sequelize.define(
'studio',
{
slug: {
type: DataTypes.STRING,
primaryKey: true
},
name: DataTypes.STRING
},
{ freezeTableName: true }
)
}
export default animation

View file

@ -0,0 +1,20 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Artist = sequelize.define(
'artist',
{
slug: {
type: DataTypes.STRING,
primaryKey: true
},
name: DataTypes.STRING
},
{
freezeTableName: true
}
)
return Artist
}
export default model

View file

@ -0,0 +1,19 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Category = sequelize.define(
'category',
{
name: {
type: DataTypes.STRING,
primaryKey: true
}
},
{
freezeTableName: true
}
)
return Category
}
export default model

View file

@ -0,0 +1,19 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Classification = sequelize.define(
'classification',
{
name: {
type: DataTypes.STRING,
primaryKey: true
}
},
{
freezeTableName: true
}
)
return Classification
}
export default model

View file

@ -0,0 +1,12 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
sequelize.define('comment', {
text: DataTypes.STRING(300),
anon: DataTypes.BOOLEAN
})
sequelize.define('rating', { score: DataTypes.INTEGER })
}
export default model

View file

@ -0,0 +1,23 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const config = sequelize.define(
'config',
{
name: {
type: DataTypes.STRING,
primaryKey: true
},
value: {
type: DataTypes.STRING,
defaultValue: ''
}
},
{
freezeTableName: true
}
)
return config
}
export default model

View file

@ -0,0 +1,11 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Disc = sequelize.define('disc', {
number: DataTypes.INTEGER,
body: DataTypes.TEXT
})
return Disc
}
export default model

View file

@ -0,0 +1,11 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Download = sequelize.define('download', {
title: DataTypes.STRING,
small: DataTypes.BOOLEAN
})
return Download
}
export default model

View file

@ -0,0 +1,27 @@
import { DataTypes } from 'sequelize'
import { PLACEHOLDER } from '@/constants'
const model = (sequelize) => {
const Game = sequelize.define(
'game',
{
slug: {
type: DataTypes.STRING,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
releaseDate: DataTypes.DATEONLY,
placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER },
headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' }
},
{
freezeTableName: true
}
)
return Game
}
export default model

View file

@ -0,0 +1,45 @@
import album from './album'
import animation from './animation'
import artist from './artist'
import category from './category'
import classification from './classification'
import comment from './comment'
import config from './config'
import disc from './disc'
import download from './download'
import game from './game'
import link from './link'
import log from './log'
import platform from './platform'
import publisher from './publisher'
import request from './request'
import role from './role'
import series from './series'
import store from './store'
import submission from './submission'
import user from './user'
const models = {
album,
animation,
artist,
category,
classification,
comment,
config,
disc,
download,
game,
link,
log,
platform,
publisher,
request,
role,
series,
store,
submission,
user
}
export default models

View file

@ -0,0 +1,13 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Link = sequelize.define('link', {
url: DataTypes.STRING,
directUrl: DataTypes.STRING,
provider: DataTypes.STRING,
custom: DataTypes.STRING
})
return Link
}
export default model

View file

@ -0,0 +1,17 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
return sequelize.define('log', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
action: DataTypes.STRING,
data: {
type: DataTypes.TEXT,
allowNull: true
}
})
}
export default model

View file

@ -0,0 +1,27 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Platform = sequelize.define(
'platform',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
type: {
type: DataTypes.STRING,
defaultValue: 'Game'
}
},
{
freezeTableName: true
}
)
return Platform
}
export default model

View file

@ -0,0 +1,23 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Publisher = sequelize.define(
'publisher',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
}
},
{
freezeTableName: true
}
)
return Publisher
}
export default model

View file

@ -0,0 +1,21 @@
import { DataTypes } from 'sequelize'
const request = (sequelize) =>
sequelize.define('request', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: DataTypes.STRING,
link: DataTypes.STRING,
user: DataTypes.STRING,
userID: DataTypes.STRING,
state: { type: DataTypes.STRING, allowNull: false },
donator: { type: DataTypes.BOOLEAN, allowNull: false },
reason: DataTypes.STRING,
comments: DataTypes.STRING,
message: DataTypes.STRING
})
export default request

View file

@ -0,0 +1,14 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Role = sequelize.define('role', {
name: {
type: DataTypes.STRING,
primaryKey: true
},
permissions: DataTypes.JSON
})
return Role
}
export default model

View file

@ -0,0 +1,24 @@
import { DataTypes } from 'sequelize'
import { PLACEHOLDER } from '@/constants'
const model = (sequelize) => {
const Series = sequelize.define(
'series',
{
slug: {
type: DataTypes.STRING,
primaryKey: true
},
name: { type: DataTypes.STRING },
placeholder: { type: DataTypes.TEXT, defaultValue: PLACEHOLDER },
headerColor: { type: DataTypes.STRING, defaultValue: '#ffffff' }
},
{
freezeTableName: true
}
)
return Series
}
export default model

View file

@ -0,0 +1,10 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const Store = sequelize.define('store', {
url: DataTypes.STRING,
provider: DataTypes.STRING
})
return Store
}
export default model

View file

@ -0,0 +1,26 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) =>
sequelize.define('submission', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
state: {
type: DataTypes.STRING,
defaultValue: 'pending'
},
title: DataTypes.STRING,
vgmdb: {
type: DataTypes.STRING,
allowNull: true
},
links: DataTypes.TEXT,
score: {
type: DataTypes.INTEGER,
defaultValue: 0
}
})
export default model

View file

@ -0,0 +1,23 @@
import { DataTypes } from 'sequelize'
const model = (sequelize) => {
const User = sequelize.define('user', {
username: {
type: DataTypes.STRING,
primaryKey: true
},
email: DataTypes.STRING,
password: DataTypes.STRING,
placeholder: { type: DataTypes.TEXT },
imgId: DataTypes.STRING
})
sequelize.define('forgor', {
key: { type: DataTypes.STRING, primaryKey: true },
expires: DataTypes.DATE
})
return User
}
export default model

View file

@ -0,0 +1,68 @@
export default function relations (sequelize) {
const {
album, classification, disc, download, link,
publisher, game, series,
platform, artist, category, store,
animation, studio,
user, role, forgor, log, comment, rating,
submission, request
} = sequelize.models
user.belongsToMany(role, { through: 'User_Role' })
log.belongsTo(user, { foreignKey: 'username' })
forgor.belongsTo(user, { foreignKey: 'username' })
submission.belongsTo(user)
submission.belongsTo(request)
user.hasMany(submission)
request.hasMany(submission)
classification.belongsToMany(album, { through: 'Album_Classification' })
disc.belongsTo(album)
download.hasMany(link)
link.belongsTo(download)
game.belongsToMany(publisher, { through: 'Publisher_Game' })
game.belongsToMany(album, { through: 'Album_Game' })
game.belongsToMany(series, { through: 'Series_Game' })
game.belongsToMany(platform, { through: 'Game_Platform' })
platform.belongsToMany(game, { through: 'Game_Platform' })
album.belongsToMany(artist, { onDelete: 'SET NULL', through: 'Album_Artist' })
album.belongsToMany(classification, { onDelete: 'SET NULL', through: 'Album_Classification' })
album.belongsToMany(category, { onDelete: 'SET NULL', through: 'Album_Category' })
album.belongsToMany(platform, { onDelete: 'SET NULL', through: 'Album_Platform' })
album.belongsToMany(game, { onDelete: 'SET NULL', through: 'Album_Game' })
album.belongsToMany(animation, { through: 'Album_Animation' })
album.hasMany(disc, { onDelete: 'SET NULL' })
album.hasMany(download, { onDelete: 'SET NULL' })
album.hasMany(store, { onDelete: 'SET NULL' })
album.belongsToMany(album, { onDelete: 'SET NULL', through: 'related_album', as: 'related' })
platform.belongsToMany(album, { through: 'Album_Platform' })
publisher.belongsToMany(game, { through: 'Publisher_Game' })
series.belongsToMany(game, { through: 'Series_Game' })
animation.belongsToMany(studio, { through: 'Studio_Animation' })
studio.belongsToMany(animation, { through: 'Studio_Animation' })
animation.belongsToMany(album, { through: 'Album_Animation' })
album.hasMany(comment, { onDelete: 'SET NULL' })
comment.belongsTo(album)
user.hasMany(comment, { foreignKey: 'username' })
comment.belongsTo(user, { foreignKey: 'username' })
album.hasMany(rating)
rating.belongsTo(album)
user.hasMany(rating, { foreignKey: 'username' })
rating.belongsTo(user, { foreignKey: 'username' })
user.belongsToMany(album, { through: 'favorites', foreignKey: 'username' })
album.belongsToMany(user, { through: 'favorites' })
}