chore: remove unused files & parts

This commit is contained in:
MTG2000
2022-10-22 11:57:32 +03:00
parent fc1a84f3bb
commit 051205c95c
120 changed files with 4 additions and 10134 deletions

View File

@@ -1,6 +1,6 @@
# Makers.Bolt.Fun
# Lightning Landscape
A lightning apps directory made for and by the bitcoin community.
A directory for lightning startups, projects, and companies.
## Environment Setup
@@ -25,48 +25,3 @@ This will spin up a local mocks server with mock data, so you can use the app of
This will assume that you have a local api server running on port 8888, and will connect to it.
In all cases, the application will be running on http://localhost:3000
# Backend API
We are using serverless functions to serve our GraphQl endpoint to the client app.
## Running locally
To run the project locally with your own local DB, you will need to first put a few env variables in an env file that should be created in /envs/server directory, named `local.env`
The required variables that needs to be put there are:
```
NODE_ENV = "development"
DATABASE_PROXY_URL = "YOUR DB CONNECTION STRING"
JWT_SECRET = "SOME RANDOM JWT SECRET"
LNURL_AUTH_HOST = "http://localhost:8888/dev/login"
CLOUDFLARE_IMAGE_ACCOUNT_ID = "FOR UPLOADING IMAGES"
CLOUDFLARE_IMAGE_API_KEY = "FOR UPLOADING IMAGES"
CLOUDFLARE_IMAGE_ACCOUNT_HASH = "FOR UPLOADING IMAGES"
```
Then you need to run the migrations against your database.
use the command:
### `npm run db:migrate-dev`
Finally, you can start the serverless functions using the command:
### `npm run server:dev`
And your functions will be served on http://localhost:8888/dev/graphql
## Database
`prisma studio`
prisma studio runs an UI for the DB
`prisma migrate dev`
Create a migration from the schema.prisma file
`prisma migrate deploy`
Apply pending migrations to the database

View File

@@ -1,132 +0,0 @@
const lnurl = require('lnurl')
const crypto = require('crypto')
const { prisma } = require('../../prisma')
const { CONSTS } = require('../../utils')
async function generateK1() {
let k1 = null
const maxAttempts = 5
let attempt = 0
while (k1 === null && attempt < maxAttempts) {
k1 = crypto.randomBytes(32).toString('hex')
const hash = createHash(k1)
const isUsed = await isHashUsed(hash);
if (isUsed) {
k1 = null
}
attempt++
}
if (!k1) {
const message = 'Too many failed attempts to generate unique k1'
throw new Error(message)
}
return k1
}
function isHashUsed(hash) {
return prisma.generatedK1.findFirst({ where: { value: hash } })
}
function addHash(hash) {
return prisma.generatedK1.create({
data: {
value: hash,
}
})
}
function removeHash(hash) {
return prisma.generatedK1.delete({
where: {
value: hash,
}
})
}
function removeExpiredHashes() {
const now = new Date();
const lastHourDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes() - 10);
return prisma.generatedK1.deleteMany({
where: {
createdAt: {
lt: lastHourDate
}
}
})
}
async function generateAuthUrl(options) {
const hostname = CONSTS.LNURL_AUTH_HOST ?? 'https://auth.bolt.fun/.netlify/functions/login';
const secret = await generateK1();
const hash = createHash(secret);
await addHash(hash)
let url = `${hostname}?tag=login&k1=${secret}`
if (options.user_token) {
url = url + `&action=link&user_token=${options.user_token}`
}
return {
url,
encoded: lnurl.encode(url).toUpperCase(),
secret,
secretHash: hash,
}
}
async function getAuthTokenByHash(hash) {
const data = await prisma.generatedK1.findFirst({
where: {
value: hash,
}
});
return data.sid;
}
function associateTokenToHash(hash, token) {
return prisma.generatedK1.update({
where: {
value: hash
},
data: {
sid: token
}
})
}
async function verifySig(sig, k1, key) {
if (!lnurl.verifyAuthorizationSignature(sig, k1, key)) {
const message = 'Signature verification failed'
throw new Error(message)
}
const hash = createHash(k1)
const hashExist = await isHashUsed(hash);
if (!hashExist)
throw new Error('Provided k1 is not issued by server')
return { key, hash }
}
function createHash(data) {
if (!(typeof data === 'string' || Buffer.isBuffer(data))) {
throw new Error(
JSON.stringify({ status: 'ERROR', reason: 'Secret must be a string or a Buffer' })
)
}
if (typeof data === 'string') {
data = Buffer.from(data, 'hex')
}
return crypto.createHash('sha256').update(data).digest('hex')
}
module.exports = {
generateAuthUrl: generateAuthUrl,
verifySig: verifySig,
removeHash: removeHash,
createHash: createHash,
removeExpiredHashes: removeExpiredHashes,
getAuthTokenByHash: getAuthTokenByHash,
associateTokenToHash: associateTokenToHash
}

View File

@@ -1,16 +0,0 @@
const { prisma } = require('../../prisma')
const getUserByPubKey = (pubKey) => {
if (!pubKey) return null;
return prisma.userKey.findUnique({
where: {
key: pubKey
},
}).user()
}
module.exports = {
getUserByPubKey,
}

View File

@@ -1,70 +0,0 @@
const LnurlAuthService = require('../../auth/services/lnurlAuth.service')
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const getLoginUrl = async (req, res) => {
const { action } = req.query;
try {
let user_token = null;
if (action === 'link') {
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey);
if (!user)
return res.status(400).json({ status: 'ERROR', reason: 'Only authenticated user can request a linking URL' });
user_token = await new jose.SignJWT({ user_id: user.id })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('5min')
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
}
const data = await LnurlAuthService.generateAuthUrl({ user_token });
const session_token = await new jose.SignJWT({ hash: data.secretHash })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('5min')
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
return res
.status(200)
.json({ ...data, session_token });
} catch (error) {
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.get('/get-login-url', getLoginUrl);
}
else {
const router = express.Router();
router.get('/get-login-url', getLoginUrl)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,40 +0,0 @@
const { ApolloServer } = require("apollo-server-lambda");
const schema = require('./schema')
const extractKeyFromCookie = require("../../utils/extractKeyFromCookie");
const server = new ApolloServer({
schema,
context: async ({ event }) => {
const userPubKey = await extractKeyFromCookie(event.headers.cookie ?? event.headers.Cookie)
return { userPubKey }
},
});
const apolloHandler = server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
credentials: true,
}
}
});
// https://github.com/vendia/serverless-express/issues/427#issuecomment-924580007
const handler = (event, context, ...args) => {
return apolloHandler(
{
...event,
requestContext: context,
},
context,
...args
);
};
exports.handler = handler;

File diff suppressed because it is too large Load Diff

View File

@@ -1,602 +0,0 @@
### This file was generated by Nexus Schema
### Do not make changes to this file directly
type Author {
avatar: String!
id: Int!
join_date: Date!
lightning_address: String
name: String!
}
type Award {
id: Int!
image: String!
project: Project!
title: String!
url: String!
}
interface BaseUser {
avatar: String!
bio: String
discord: String
github: String
id: Int!
in_tournament(id: Int!): Boolean!
jobTitle: String
join_date: Date!
lightning_address: String
linkedin: String
location: String
name: String!
projects: [Project!]!
role: String
roles: [MakerRole!]!
similar_makers: [User!]!
skills: [MakerSkill!]!
stories: [Story!]!
tournaments: [Tournament!]!
twitter: String
website: String
}
type Bounty implements PostBase {
applicants_count: Int!
applications: [BountyApplication!]!
author: Author!
body: String!
cover_image: String
createdAt: Date!
deadline: String!
excerpt: String!
id: Int!
is_published: Boolean
reward_amount: Int!
tags: [Tag!]!
title: String!
type: String!
updatedAt: Date!
votes_count: Int!
}
type BountyApplication {
author: Author!
date: String!
id: Int!
workplan: String!
}
type Capability {
icon: String!
id: Int!
title: String!
}
type Category {
apps_count: Int!
cover_image: String
icon: String
id: Int!
project: [Project!]!
title: String!
votes_sum: Int!
}
input CreateProjectInput {
capabilities: [Int!]!
category_id: Int!
cover_image: ImageInput!
description: String!
discord: String
github: String
hashtag: String!
id: Int
launch_status: ProjectLaunchStatusEnum!
lightning_address: String
members: [TeamMemberInput!]!
recruit_roles: [Int!]!
screenshots: [ImageInput!]!
slack: String
tagline: String!
telegram: String
thumbnail_image: ImageInput!
title: String!
tournaments: [Int!]!
twitter: String
website: String!
}
type CreateProjectResponse {
project: Project!
}
"""Date custom scalar type"""
scalar Date
type Donation {
amount: Int!
by: User
createdAt: Date!
id: Int!
paid: Boolean!
payment_hash: String!
payment_request: String!
}
type DonationsStats {
applications: String!
donations: String!
prizes: String!
touranments: String!
}
type GenericMakerRole {
icon: String!
id: Int!
title: String!
}
type Hackathon {
cover_image: String!
description: String!
end_date: Date!
id: Int!
location: String!
start_date: Date!
tags: [Tag!]!
title: String!
website: String!
}
input ImageInput {
id: String
name: String
url: String!
}
type LnurlDetails {
commentAllowed: Int
maxSendable: Int
metadata: String
minSendable: Int
}
type MakerRole {
icon: String!
id: Int!
level: RoleLevelEnum!
title: String!
}
input MakerRoleInput {
id: Int!
level: RoleLevelEnum!
}
type MakerSkill {
id: Int!
title: String!
}
input MakerSkillInput {
id: Int!
}
type Mutation {
confirmDonation(payment_request: String!, preimage: String!): Donation!
confirmVote(payment_request: String!, preimage: String!): Vote!
createProject(input: CreateProjectInput): CreateProjectResponse
createStory(data: StoryInputType): Story
deleteProject(id: Int!): Project
deleteStory(id: Int!): Story
donate(amount_in_sat: Int!): Donation!
registerInTournament(data: RegisterInTournamentInput, tournament_id: Int!): User
updateProfileDetails(data: ProfileDetailsInput): MyProfile
updateProfileRoles(data: ProfileRolesInput): MyProfile
updateProject(input: UpdateProjectInput): CreateProjectResponse
updateTournamentRegistration(data: UpdateTournamentRegistrationInput, tournament_id: Int!): ParticipationInfo
updateUserPreferences(userKeys: [UserKeyInputType!]): MyProfile!
vote(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote!
}
type MyProfile implements BaseUser {
avatar: String!
bio: String
discord: String
email: String
github: String
id: Int!
in_tournament(id: Int!): Boolean!
jobTitle: String
join_date: Date!
lightning_address: String
linkedin: String
location: String
name: String!
nostr_prv_key: String
nostr_pub_key: String
projects: [Project!]!
role: String
roles: [MakerRole!]!
similar_makers: [User!]!
skills: [MakerSkill!]!
stories: [Story!]!
tournaments: [Tournament!]!
twitter: String
walletsKeys: [WalletKey!]!
website: String
}
enum POST_TYPE {
Bounty
Question
Story
}
type ParticipationInfo {
createdAt: Date!
email: String!
hacking_status: TournamentMakerHackingStatusEnum!
}
union Post = Bounty | Question | Story
interface PostBase {
body: String!
createdAt: Date!
excerpt: String!
id: Int!
is_published: Boolean
title: String!
updatedAt: Date!
votes_count: Int!
}
type PostComment {
author: Author!
body: String!
created_at: Date!
id: Int!
parentId: Int
votes_count: Int!
}
input ProfileDetailsInput {
avatar: ImageInput
bio: String
discord: String
email: String
github: String
jobTitle: String
lightning_address: String
linkedin: String
location: String
name: String
twitter: String
website: String
}
input ProfileRolesInput {
roles: [MakerRoleInput!]!
skills: [MakerSkillInput!]!
}
type Project {
awards: [Award!]!
capabilities: [Capability!]!
category: Category!
cover_image: String!
description: String!
discord: String
github: String
hashtag: String!
id: Int!
launch_status: ProjectLaunchStatusEnum!
lightning_address: String
lnurl_callback_url: String
members: [ProjectMember!]!
permissions: [ProjectPermissionEnum!]!
recruit_roles: [MakerRole!]!
screenshots: [String!]!
slack: String
stories: [Story!]!
tagline: String!
tags: [Tag!]!
telegram: String
thumbnail_image: String!
title: String!
tournaments: [Tournament!]!
twitter: String
votes_count: Int!
website: String!
}
enum ProjectLaunchStatusEnum {
Launched
WIP
}
type ProjectMember {
role: TEAM_MEMBER_ROLE!
user: User!
}
enum ProjectPermissionEnum {
DeleteProject
UpdateAdmins
UpdateInfo
UpdateMembers
}
type Query {
allCategories: [Category!]!
allProjects(skip: Int = 0, take: Int = 50): [Project!]!
checkValidProjectHashtag(hashtag: String!, projectId: Int): Boolean!
getAllCapabilities: [Capability!]!
getAllHackathons(sortBy: String, tag: Int): [Hackathon!]!
getAllMakersRoles: [GenericMakerRole!]!
getAllMakersSkills: [MakerSkill!]!
getCategory(id: Int!): Category!
getDonationsStats: DonationsStats!
getFeed(skip: Int = 0, sortBy: String, tag: Int = 0, take: Int = 10): [Post!]!
getLnurlDetailsForProject(project_id: Int!): LnurlDetails!
getMakersInTournament(openToConnect: Boolean, roleId: Int, search: String, skip: Int = 0, take: Int = 10, tournamentId: Int!): TournamentMakersResponse!
getMyDrafts(type: POST_TYPE!): [Post!]!
getPostById(id: Int!, type: POST_TYPE!): Post!
getProject(id: Int, tag: String): Project!
getProjectsInTournament(roleId: Int, search: String, skip: Int = 0, take: Int = 10, tournamentId: Int!): TournamentProjectsResponse!
getTournamentById(id: Int!): Tournament!
getTournamentToRegister: [Tournament!]!
getTrendingPosts: [Post!]!
hottestProjects(skip: Int = 0, take: Int = 50): [Project!]!
me: MyProfile
newProjects(skip: Int = 0, take: Int = 50): [Project!]!
officialTags: [Tag!]!
popularTags: [Tag!]!
profile(id: Int!): User
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
searchUsers(value: String!): [User!]!
similarMakers(id: Int!): [User!]!
similarProjects(id: Int!): [Project!]!
tournamentParticipationInfo(tournamentId: Int!): ParticipationInfo
}
type Question implements PostBase {
author: Author!
body: String!
createdAt: Date!
excerpt: String!
id: Int!
is_published: Boolean
tags: [Tag!]!
title: String!
type: String!
updatedAt: Date!
votes_count: Int!
}
input RegisterInTournamentInput {
email: String!
hacking_status: TournamentMakerHackingStatusEnum!
}
enum RoleLevelEnum {
Advanced
Beginner
Hobbyist
Intermediate
Pro
}
type Story implements PostBase {
author: Author!
body: String!
comments: [PostComment!]!
comments_count: Int!
cover_image: String
createdAt: Date!
excerpt: String!
id: Int!
is_published: Boolean
project: Project
tags: [Tag!]!
title: String!
type: String!
updatedAt: Date!
votes_count: Int!
}
input StoryInputType {
body: String!
cover_image: ImageInput
id: Int
is_published: Boolean
project_id: Int
tags: [String!]!
title: String!
}
enum TEAM_MEMBER_ROLE {
Admin
Maker
Owner
}
type Tag {
description: String
icon: String
id: Int!
isOfficial: Boolean
title: String!
}
input TeamMemberInput {
id: Int!
role: TEAM_MEMBER_ROLE!
}
type Tournament {
cover_image: String!
description: String!
end_date: Date!
events: [TournamentEvent!]!
events_count: Int!
faqs: [TournamentFAQ!]!
id: Int!
judges: [TournamentJudge!]!
location: String!
makers_count: Int!
prizes: [TournamentPrize!]!
projects_count: Int!
start_date: Date!
thumbnail_image: String!
title: String!
website: String!
}
type TournamentEvent {
description: String!
ends_at: Date!
id: Int!
image: String!
links: [String!]!
location: String!
starts_at: Date!
title: String!
type: TournamentEventTypeEnum!
website: String!
}
enum TournamentEventTypeEnum {
IRLMeetup
OnlineMeetup
TwitterSpace
Workshop
}
type TournamentFAQ {
answer: String!
question: String!
}
type TournamentJudge {
avatar: String!
company: String!
name: String!
}
enum TournamentMakerHackingStatusEnum {
OpenToConnect
Solo
}
type TournamentMakersResponse {
hasNext: Boolean
hasPrev: Boolean
makers: [TournamentParticipant!]!
}
type TournamentParticipant {
hacking_status: TournamentMakerHackingStatusEnum!
is_registered: Boolean
user: User!
}
type TournamentPrize {
amount: String!
image: String!
title: String!
}
type TournamentProjectsResponse {
hasNext: Boolean
hasPrev: Boolean
projects: [Project!]!
}
input UpdateProjectInput {
capabilities: [Int!]!
category_id: Int!
cover_image: ImageInput!
description: String!
discord: String
github: String
hashtag: String!
id: Int
launch_status: ProjectLaunchStatusEnum!
lightning_address: String
members: [TeamMemberInput!]!
recruit_roles: [Int!]!
screenshots: [ImageInput!]!
slack: String
tagline: String!
telegram: String
thumbnail_image: ImageInput!
title: String!
tournaments: [Int!]!
twitter: String
website: String!
}
input UpdateTournamentRegistrationInput {
email: String
hacking_status: TournamentMakerHackingStatusEnum
}
type User implements BaseUser {
avatar: String!
bio: String
discord: String
github: String
id: Int!
in_tournament(id: Int!): Boolean!
jobTitle: String
join_date: Date!
lightning_address: String
linkedin: String
location: String
name: String!
projects: [Project!]!
role: String
roles: [MakerRole!]!
similar_makers: [User!]!
skills: [MakerSkill!]!
stories: [Story!]!
tournaments: [Tournament!]!
twitter: String
website: String
}
input UserKeyInputType {
key: String!
name: String!
}
enum VOTE_ITEM_TYPE {
Bounty
PostComment
Project
Question
Story
User
}
type Vote {
amount_in_sat: Int!
id: Int!
item_id: Int!
item_type: VOTE_ITEM_TYPE!
paid: Boolean!
payment_hash: String!
payment_request: String!
}
type WalletKey {
createdAt: Date!
is_current: Boolean!
key: String!
name: String!
}

View File

@@ -1,35 +0,0 @@
const { makeSchema } = require('nexus');
const { join } = require('path');
const types = require('../types')
const schema = makeSchema({
types: types,
outputs: {
typegen: join(__dirname, '..', 'nexus-typegen.ts'),
schema: join(__dirname, '..', 'schema.graphql'),
},
})
module.exports = schema;
// const { gql } = require("apollo-server-lambda");
// const projectSchema = require('./project')
// const categorySchema = require('./category')
// const voteSchema = require('./vote')
// const linkSchema = gql`
// type Query {
// _: Boolean
// }
// type Mutation {
// _: Boolean
// }
// type Subscription {
// _: Boolean
// }
// `;
// module.exports = [linkSchema, categorySchema, projectSchema, voteSchema];

View File

@@ -1,24 +0,0 @@
const { Kind } = require("graphql")
const { scalarType } = require("nexus")
const DateScalar = scalarType({
name: 'Date',
asNexusMethod: 'date',
description: 'Date custom scalar type',
parseValue(value) {
return new Date(value)
},
serialize(value) {
return value.toISOString()
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return new Date(ast.value)
}
return null
},
})
module.exports = {
DateScalar
}

View File

@@ -1,95 +0,0 @@
const {
intArg,
objectType,
extendType,
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Category = objectType({
name: 'Category',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.string('cover_image', {
async resolve(parent) {
return prisma.category.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
}
});
t.string('icon');
t.nonNull.int('votes_sum', {
async resolve(parent) {
const projects = await prisma.category.findUnique({ where: { id: parent.id } }).project()
return projects.reduce((total, project) => total + project.votes_count, 0);
}
});
t.nonNull.int('apps_count', {
async resolve(parent) {
const projects = await prisma.category.findUnique({ where: { id: parent.id } }).project();
return projects.length;
}
});
t.nonNull.list.nonNull.field('project', {
type: "Project",
resolve: (parent) => {
return parent.project ?? prisma.category.findUnique({
where: { id: parent.id }
}).project()
}
})
}
})
const allCategoriesQuery = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('allCategories', {
type: "Category",
resolve: async () => {
const categories = await prisma.category.findMany({
include: {
_count: {
select: {
project: true
}
}
}
})
categories.sort((c1, c2) => c2._count.project - c1._count.project)
return categories;
}
})
}
})
const getCategory = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getCategory', {
type: "Category",
args: {
id: nonNull(intArg())
},
resolve(parent, { id }) {
return prisma.category.findUnique({
where: { id }
})
}
})
}
})
module.exports = {
// Types
Category,
// Queries
allCategoriesQuery,
getCategory
}

View File

@@ -1,154 +0,0 @@
const { createHash } = require('crypto');
const { parsePaymentRequest } = require('invoices');
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const { CONSTS } = require('../../../utils');
const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers');
const Donation = objectType({
name: 'Donation',
definition(t) {
t.nonNull.int('id');
t.nonNull.int('amount');
t.nonNull.date('createdAt');
t.nonNull.string('payment_request');
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.field('by', {
type: 'User',
resolve: (parent) => {
return prisma.donation.findUnique({ where: { id: parent.id } }).donor();
}
});
}
})
const donateMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('donate', {
type: "Donation",
args: {
amount_in_sat: nonNull(intArg())
},
resolve: async (_, args) => {
const { amount_in_sat } = args;
const lightning_address = CONSTS.BOLT_FUN_LIGHTNING_ADDRESS;
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
const invoice = parsePaymentRequest({ request: pr });
return prisma.donation.create({
data: {
amount: amount_in_sat,
payment_request: pr,
payment_hash: invoice.id,
}
});
}
})
}
})
const confirmDonateMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('confirmDonation', {
type: "Donation",
args: {
payment_request: nonNull(stringArg()),
preimage: nonNull(stringArg())
},
resolve: async (_, args) => {
const paymentHash = createHash("sha256")
.update(hexToUint8Array(args.preimage))
.digest("hex");
// look for a vote for the payment request and the calculated payment hash
const donation = await prisma.donation.findFirst({
where: {
payment_request: args.payment_request,
payment_hash: paymentHash,
},
});
// if we find a donation it means the preimage is correct and we update the donation and mark it as paid
// can we write this nicer?
if (donation) {
// return the current donation
return prisma.donation.update({
where: { id: donation.id },
data: {
paid: true,
preimage: args.preimage,
}
});
} else {
throw new Error("Invalid preimage");
}
}
})
}
})
const DonationsStats = objectType({
name: 'DonationsStats',
definition(t) {
t.nonNull.string("prizes");
t.nonNull.string("touranments");
t.nonNull.string("donations");
t.nonNull.string("applications");
},
})
const getDonationsStats = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getDonationsStats', {
type: "DonationsStats",
async resolve() {
const [donations, applications] = await Promise.all([
prisma.donation.aggregate({
_sum: {
amount: true
},
where: {
paid: true
}
}).then(d => d._sum.amount ?? 0),
prisma.project.count()]);
// #TODO add a measurement unit for prizes & donations (eg. $ or sats or BTC)
return {
prizes: '$5.2k',
touranments: 2,
donations,
applications
}
}
})
}
})
module.exports = {
// Types
Donation,
DonationsStats,
// Queries
donateMutation,
confirmDonateMutation,
getDonationsStats,
}

View File

@@ -1,89 +0,0 @@
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
} = require('nexus');
const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Hackathon = objectType({
name: 'Hackathon',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('cover_image', {
async resolve(parent) {
return prisma.hackathon.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.date('start_date');
t.nonNull.date('end_date');
t.nonNull.string('location');
t.nonNull.string('website');
t.nonNull.list.nonNull.field('tags', {
type: "Tag",
resolve: (parent) => {
return prisma.hackathon.findUnique({ where: { id: parent.id } }).tags();
}
});
}
})
const getAllHackathons = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getAllHackathons', {
type: "Hackathon",
args: {
sortBy: stringArg(),
tag: intArg(),
},
resolve(_, args) {
const { sortBy, tag } = args;
return prisma.hackathon.findMany({
where: {
...(sortBy === 'Upcoming' && {
start_date: {
gte: new Date(),
}
}),
...(sortBy === 'Live' && {
start_date: { lte: new Date() },
end_date: { gte: new Date() }
}),
...(sortBy === 'Finished' && {
end_date: {
lt: new Date()
}
}),
...(tag && {
tags: {
some: {
id: tag
}
}
})
},
orderBy: {
start_date: "desc"
}
})
}
})
}
})
module.exports = {
// Types
Hackathon,
// Queries
getAllHackathons,
}

View File

@@ -1,86 +0,0 @@
const { intArg } = require("nexus");
const axios = require("axios");
function hexToUint8Array(hexString) {
const match = hexString.match(/.{1,2}/g);
if (match) {
return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
}
}
// TODO: generaly validate LNURL responses
// get lnurl params
function getLnurlDetails(lnurl) {
return axios.get(lnurl);
}
// parse lightning address and return a url that can be
// used in a request
function lightningAddressToLnurl(lightning_address) {
const [name, domain] = lightning_address.split("@");
return `https://${domain}/.well-known/lnurlp/${name}`;
}
// when pressing tip or selecting an amount.
// this is used for caching so the frontend doesnt
// have to make an additional http request to get
// the callback url for future visits
async function getLnurlCallbackUrl(lightning_address) {
return getLnurlDetails(lightningAddressToLnurl(lightning_address)).then(
(response) => {
return response.data.callback;
}
);
}
async function getPaymetRequestForItem(lightning_address, amount_in_sat) {
// # NOTE: CACHING LNURL CALLBACK URLS + PARAMETERS
// LNURL flows have a lot of back and forth and can impact
// the load time for your application users.
// You may consider caching the callback url, or resolved
// parameters but be mindful of this.
// The LNURL service provider can change the callback url
// details or the paramters that is returned we must be
// careful when trying to optimise the amount of
// requests so be mindful of this when you are storing
// these items.
const amount = amount_in_sat * 1000; // msats
let lnurlCallbackUrl = await getLnurlCallbackUrl(lightning_address);
return axios
.get(lnurlCallbackUrl, { params: { amount } })
.then((prResponse) => {
return prResponse.data.pr;
});
}
const paginationArgs = (args) => {
const { take = 10, skip = 0 } = args ?? {}
return {
take: intArg({ default: take }),
skip: intArg({ default: skip })
}
}
const removeNulls = (obj) => {
let res = {};
for (const key in obj) {
if (obj[key] != null) {
res[key] = obj[key];
}
}
return res
}
module.exports = {
getPaymetRequestForItem,
hexToUint8Array,
lightningAddressToLnurl,
getLnurlDetails,
paginationArgs,
removeNulls
}

View File

@@ -1,25 +0,0 @@
const scalars = require('./_scalars')
const misc = require('./misc')
const category = require('./category')
const project = require('./project')
const vote = require('./vote')
const post = require('./post')
const users = require('./users')
const hackathon = require('./hackathon')
const tournament = require('./tournament')
const donation = require('./donation')
const tag = require('./tag')
module.exports = {
...misc,
...tag,
...scalars,
...category,
...project,
...vote,
...post,
...users,
...hackathon,
...tournament,
...donation,
}

View File

@@ -1,19 +0,0 @@
const { objectType, extendType, inputObjectType } = require("nexus");
const { prisma } = require('../../../prisma');
const ImageInput = inputObjectType({
name: 'ImageInput',
definition(t) {
t.string('id');
t.string('name');
t.nonNull.string('url');
}
});
module.exports = {
// Types
ImageInput,
// Queries
}

View File

@@ -1,698 +0,0 @@
const {
intArg,
objectType,
extendType,
nonNull,
interfaceType,
unionType,
stringArg,
enumType,
arg,
inputObjectType,
} = require('nexus');
const { paginationArgs } = require('./helpers');
const { prisma } = require('../../../prisma');
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { ApolloError } = require('apollo-server-lambda');
const { marked } = require('marked');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { ImageInput } = require('./misc');
const { deleteImage } = require('../../../services/imageUpload.service');
const { logError } = require('../../../utils/logger');
const POST_TYPE = enumType({
name: 'POST_TYPE',
members: ['Story', 'Bounty', 'Question'],
});
const asType = type => (obj) => {
if (Array.isArray(obj)) return obj.map(o => ({ ...o, type }))
return { ...obj, type }
}
const asStoryType = asType('Story')
const asQuestionType = asType('Question')
const asBountyType = asType('Bounty')
const Author = objectType({
name: 'Author',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
t.nonNull.string('avatar', {
async resolve(parent) {
return prisma.user.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.date('join_date');
t.string('lightning_address');
}
})
const PostBase = interfaceType({
name: 'PostBase',
resolveType(item) {
return item.type
},
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.date('createdAt');
t.nonNull.date('updatedAt');
t.nonNull.string('body');
t.nonNull.string('excerpt');
t.nonNull.int('votes_count');
t.boolean('is_published');
},
})
const Story = objectType({
name: 'Story',
definition(t) {
t.implements('PostBase');
t.nonNull.string('type', {
resolve: () => t.typeName
});
t.string('cover_image', {
async resolve(parent) {
return prisma.story.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.list.nonNull.field('comments', {
type: "PostComment",
resolve: (parent) => []
});
t.nonNull.list.nonNull.field('tags', {
type: "Tag",
resolve: (parent) => prisma.story.findUnique({ where: { id: parent.id } }).tags()
});
t.nonNull.int('comments_count', {
resolve: async (parent) => {
const post = await prisma.story.findUnique({
where: { id: parent.id },
include: {
_count: {
select: {
comments: true
}
}
}
})
return post._count.comments;
}
});
t.nonNull.field('author', {
type: "Author",
resolve: (parent) =>
prisma.story.findUnique({ where: { id: parent.id } }).user()
});
t.field('project', {
type: "Project",
resolve(parent) {
return prisma.story.findUnique({ where: { id: parent.id } }).project();
}
})
},
})
const StoryInputType = inputObjectType({
name: 'StoryInputType',
definition(t) {
t.int('id');
t.nonNull.string('title');
t.nonNull.string('body');
t.field('cover_image', {
type: ImageInput
})
t.nonNull.list.nonNull.string('tags');
t.boolean('is_published')
t.int('project_id')
}
})
const BountyApplication = objectType({
name: 'BountyApplication',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('date');
t.nonNull.string('workplan');
t.nonNull.field('author', {
type: "Author"
});
}
})
const Bounty = objectType({
name: 'Bounty',
definition(t) {
t.implements('PostBase');
t.nonNull.string('type', {
resolve: () => 'Bounty'
});
t.string('cover_image');
t.nonNull.string('deadline');
t.nonNull.int('reward_amount');
t.nonNull.int('applicants_count');
t.nonNull.list.nonNull.field('applications', {
type: "BountyApplication"
});
t.nonNull.field('author', {
type: "Author",
resolve: (parent) => {
return prisma.bounty.findUnique({ where: { id: parent.id } }).user();
}
});
t.nonNull.list.nonNull.field('tags', {
type: "Tag",
resolve: (parent) => []
});
},
})
const Question = objectType({
name: 'Question',
definition(t) {
t.implements('PostBase');
t.nonNull.string('type', {
resolve: () => 'Question',
});
t.nonNull.list.nonNull.field('tags', {
type: "Tag",
resolve: (parent) => prisma.question.findUnique({ where: { id: parent.id } }).tags()
});
// t.nonNull.int('answers_count');
// t.nonNull.list.nonNull.field('comments', {
// type: "PostComment",
// resolve: (parent) => {
// return prisma.question.findUnique({ where: { id: parent.id } }).comments();
// }
// });
t.nonNull.field('author', {
type: "Author",
resolve: (parent) => {
return prisma.question.findUnique({ where: { id: parent.id } }).user();
}
});
},
})
const PostComment = objectType({
name: 'PostComment',
definition(t) {
t.nonNull.int('id');
t.nonNull.date('created_at');
t.nonNull.string('body');
t.nonNull.field('author', {
type: "Author"
});
t.int('parentId');
t.nonNull.int('votes_count');
}
})
const Post = unionType({
name: 'Post',
definition(t) {
t.members('Story', 'Bounty', 'Question')
},
resolveType: (item) => {
return item.type
},
})
const getFeed = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getFeed', {
type: "Post",
args: {
...paginationArgs({ take: 10 }),
sortBy: stringArg(), // all, popular, trending, newest
tag: intArg({
default: 0
})
},
resolve(_, { take, skip, tag, sortBy, }) {
let orderBy = { createdAt: "desc" };
if (sortBy === 'popular')
orderBy = { votes_count: 'desc' };
else if (sortBy === 'newest')
orderBy = { createdAt: "desc" };
return prisma.story.findMany({
orderBy: orderBy,
where: {
...(tag && {
tags: {
some: {
id: tag
}
},
}),
is_published: true,
},
skip,
take,
}).then(asStoryType)
}
})
}
})
const getTrendingPosts = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getTrendingPosts', {
type: "Post",
args: {
},
resolve() {
const now = new Date();
const lastWeekDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
return prisma.story.findMany({
where: {
createdAt: {
gte: lastWeekDate
},
is_published: true,
},
orderBy: { votes_count: 'desc' },
take: 5,
}).then(asStoryType)
}
})
}
})
const getMyDrafts = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getMyDrafts', {
type: "Post",
args: {
type: arg({
type: nonNull('POST_TYPE')
})
},
async resolve(parent, { type }, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
if (type === 'Story')
return prisma.story.findMany({
where: {
is_published: false,
user_id: user.id
},
orderBy: { createdAt: 'desc' },
}).then(asStoryType)
return []
}
})
}
})
const getPostById = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getPostById', {
type: "Post",
args: {
id: nonNull(intArg()),
type: arg({
type: nonNull('POST_TYPE')
})
},
resolve(_, { id, type }) {
if (type === 'Story')
return prisma.story.findUnique({
where: { id }
}).then(asStoryType)
if (type === 'Question')
return prisma.question.findUnique({
where: { id }
}).then(asQuestionType)
return null
}
})
}
})
const addCoverImage = async (providerImageId) => {
const newCoverImage = await prisma.hostedImage.findFirst({
where: {
provider_image_id: providerImageId
}
})
if (!newCoverImage) throw new ApolloError("New cover image not found")
await prisma.hostedImage.update({
where: {
id: newCoverImage.id
},
data: {
is_used: true
}
})
return newCoverImage
}
const getHostedImageIdsFromBody = async (body, oldBodyImagesIds = null) => {
let bodyImageIds = []
const regex = /(?:!\[(.*?)\]\((.*?)\))/g
let match;
while ((match = regex.exec(body))) {
const [, , value] = match
// Useful for old external images in case of duplicates. We need to be sure we are targeting an image from the good story.
const where = oldBodyImagesIds ? {
AND: [
{ url: value },
{ id: { in: oldBodyImagesIds } }
]
} :
{
url: value,
}
const hostedImage = await prisma.hostedImage.findFirst({
where
})
if (hostedImage) {
bodyImageIds.push(hostedImage.id)
await prisma.hostedImage.update({
where: {
id: hostedImage.id
},
data: {
is_used: true
}
})
}
}
return bodyImageIds
}
const createStory = extendType({
type: 'Mutation',
definition(t) {
t.field('createStory', {
type: 'Story',
args: { data: StoryInputType },
async resolve(_root, args, ctx) {
const { id, title, body, project_id, cover_image, tags, is_published } = args.data;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
let was_published = false;
// TODO: validate post data
let coverImage = null
let bodyImageIds = []
try {
if (id) {
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true,
is_published: true,
cover_image_id: true,
body_image_ids: true
}
})
was_published = oldPost.is_published;
if (user.id !== oldPost.user_id) throw new ApolloError("Not post author")
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body, oldPost.body_image_ids)
// Old cover image is found
if (oldPost.cover_image_id) {
const oldCoverImage = await prisma.hostedImage.findFirst({
where: {
id: oldPost.cover_image_id
}
})
// New cover image
if (cover_image?.id && cover_image.id !== oldCoverImage?.provider_image_id) {
await deleteImage(oldCoverImage.id)
coverImage = await addCoverImage(cover_image.id)
} else {
coverImage = oldCoverImage
}
} else {
// No old image found and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Remove unused body images
const unusedImagesIds = oldPost.body_image_ids.filter(x => !bodyImageIds.includes(x));
unusedImagesIds.map(async i => await deleteImage(i))
} else {
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body)
// New story and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Preprocess & insert
const htmlBody = marked.parse(body);
const excerpt = htmlBody
.replace(/<[^>]+>/g, '')
.slice(0, 120)
.replace(/&amp;/g, "&")
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"')
;
const coverImageRel = coverImage ? {
cover_image_rel: {
connect:
{
id: coverImage ? coverImage.id : null
}
}
} : {}
if (id) {
await prisma.story.update({
where: { id },
data: {
tags: {
set: []
},
}
});
return prisma.story.update({
where: { id },
data: {
title,
body,
cover_image: '',
excerpt,
is_published: was_published || is_published,
project: project_id ? {
connect: {
id: project_id,
},
} : {
disconnect: true
},
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
body_image_ids: bodyImageIds,
...coverImageRel
}
})
.catch(error => {
logError(error)
throw new ApolloError("Unexpected error happened...")
})
}
return prisma.story.create({
data: {
title,
body,
cover_image: '',
excerpt,
is_published,
tags: {
connectOrCreate:
tags.map(tag => {
tag = tag.toLowerCase().trim();
return {
where: {
title: tag,
},
create: {
title: tag
}
}
})
},
project: {
connect: {
id: project_id,
}
},
user: {
connect: {
id: user.id,
}
},
body_image_ids: bodyImageIds,
...coverImageRel
}
}).catch(error => {
logError(error)
throw new ApolloError("Unexpected error happened...")
})
} catch (error) {
logError(error)
throw new ApolloError("Unexpected error happened...")
}
}
})
},
})
const deleteStory = extendType({
type: 'Mutation',
definition(t) {
t.field('deleteStory', {
type: 'Story',
args: { id: nonNull(intArg()) },
async resolve(_root, args, ctx) {
const { id } = args;
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new ApolloError("Not Authenticated");
const oldPost = await prisma.story.findFirst({
where: { id },
select: {
user_id: true,
body_image_ids: true,
cover_image_id: true
}
})
if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author")
const deletedPost = await prisma.story.delete({
where: {
id
}
})
const coverImage = await prisma.hostedImage.findMany({
where: {
OR: [
{ id: oldPost.cover_image_id },
{
id: {
in: oldPost.body_image_ids
}
}
]
},
select: {
id: true,
provider_image_id: true
}
})
coverImage.map(async i => await deleteImage(i.id))
return deletedPost
}
})
},
})
module.exports = {
// Types
POST_TYPE,
Author,
PostBase,
BountyApplication,
Bounty,
Story,
StoryInputType,
Question,
PostComment,
Post,
// Queries
getFeed,
getPostById,
getTrendingPosts,
getMyDrafts,
// Mutations
createStory,
deleteStory,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +0,0 @@
const { objectType, extendType } = require("nexus");
const { prisma } = require('../../../prisma');
const Tag = objectType({
name: 'Tag',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.string('icon');
t.string('description');
t.boolean('isOfficial');
}
});
const officialTags = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('officialTags', {
type: "Tag",
resolve: () => {
return prisma.tag.findMany({
orderBy: {
title: 'asc'
},
where: {
isOfficial: true
}
});
}
})
}
})
const popularTags = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('popularTags', {
type: "Tag",
resolve: () => {
return prisma.tag.findMany({
where: {
isOfficial: true
},
take: 8,
orderBy: {
stories: {
_count: 'desc'
}
},
});
}
})
}
})
module.exports = {
// Types
Tag,
// Queries
popularTags,
officialTags,
}

View File

@@ -1,566 +0,0 @@
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
enumType,
inputObjectType,
booleanArg,
} = require('nexus');
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { prisma } = require('../../../prisma');
const { paginationArgs, removeNulls } = require('./helpers');
const TournamentPrize = objectType({
name: 'TournamentPrize',
definition(t) {
t.nonNull.string('title');
t.nonNull.string('amount');
t.nonNull.string('image', {
async resolve(parent) {
return prisma.tournamentPrize.findUnique({ where: { id: parent.id } }).image_rel().then(resolveImgObjectToUrl)
}
});
}
})
const TournamentJudge = objectType({
name: 'TournamentJudge',
definition(t) {
t.nonNull.string('name');
t.nonNull.string('company');
t.nonNull.string('avatar', {
async resolve(parent) {
return prisma.tournamentJudge.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
}
});
}
})
const TournamentFAQ = objectType({
name: 'TournamentFAQ',
definition(t) {
t.nonNull.string('question');
t.nonNull.string('answer');
}
})
const TournamentParticipant = objectType({
name: "TournamentParticipant",
definition(t) {
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum });
t.boolean('is_registered')
t.nonNull.field('user', { type: "User" })
}
})
const TournamentEventTypeEnum = enumType({
name: 'TournamentEventTypeEnum',
members: {
TwitterSpace: 0,
Workshop: 1,
IRLMeetup: 2,
OnlineMeetup: 3,
},
});
const TournamentMakerHackingStatusEnum = enumType({
name: 'TournamentMakerHackingStatusEnum',
members: {
Solo: 0,
OpenToConnect: 1,
},
});
const TournamentEvent = objectType({
name: 'TournamentEvent',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('image', {
async resolve(parent) {
return prisma.tournamentEvent.findUnique({ where: { id: parent.id } }).image_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.string('description');
t.nonNull.date('starts_at');
t.nonNull.date('ends_at');
t.nonNull.string('location');
t.nonNull.string('website');
t.nonNull.field('type', { type: TournamentEventTypeEnum })
t.nonNull.list.nonNull.string('links', { resolve() { return [] } });
}
})
const Tournament = objectType({
name: 'Tournament',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('thumbnail_image', {
async resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).thumbnail_image_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.string('cover_image', {
async resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).cover_image_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.date('start_date');
t.nonNull.date('end_date');
t.nonNull.string('location');
t.nonNull.string('website');
t.nonNull.int('events_count', {
resolve(parent) {
return prisma.tournamentEvent.count({
where: {
tournament_id: parent.id
}
})
}
});
t.nonNull.int('makers_count', {
resolve(parent) {
return prisma.tournamentParticipant.count({
where: {
tournament_id: parent.id
}
})
}
});
t.nonNull.int('projects_count', {
resolve(parent) {
return prisma.tournamentProject.count({
where: {
tournament_id: parent.id
}
})
}
});
t.nonNull.list.nonNull.field('prizes', {
type: TournamentPrize,
resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).prizes()
}
});
t.nonNull.list.nonNull.field('judges', {
type: TournamentJudge,
resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).judges()
}
});
t.nonNull.list.nonNull.field('faqs', {
type: TournamentFAQ,
resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).faqs()
}
});
t.nonNull.list.nonNull.field('events', {
type: TournamentEvent,
resolve(parent) {
return prisma.tournament.findUnique({ where: { id: parent.id } }).events()
}
});
}
})
const TournamentMakersResponse = objectType({
name: 'TournamentMakersResponse',
definition(t) {
t.boolean('hasNext');
t.boolean('hasPrev');
t.nonNull.list.nonNull.field('makers', { type: TournamentParticipant })
}
}
)
const TournamentProjectsResponse = objectType({
name: 'TournamentProjectsResponse',
definition(t) {
t.boolean('hasNext');
t.boolean('hasPrev');
t.nonNull.list.nonNull.field('projects', { type: "Project" })
}
}
)
const getTournamentById = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getTournamentById', {
type: Tournament,
args: {
id: nonNull(intArg()),
},
resolve(_, { id }) {
return prisma.tournament.findUnique({
where: { id }
})
}
})
}
})
const getTournamentToRegister = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getTournamentToRegister', {
type: Tournament,
args: {
},
resolve() {
return prisma.tournament.findMany({
where: {
end_date: {
gt: new Date()
},
}
})
}
})
}
})
const ParticipationInfo = objectType({
name: "ParticipationInfo",
definition(t) {
t.nonNull.date('createdAt')
t.nonNull.string('email')
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum });
}
})
const tournamentParticipationInfo = extendType({
type: "Query",
definition(t) {
t.field('tournamentParticipationInfo', {
type: ParticipationInfo,
args: {
tournamentId: nonNull(intArg()),
},
async resolve(_, args, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
if (!user)
return null
return prisma.tournamentParticipant.findFirst({
where: {
user_id: user.id,
tournament_id: args.tournamentId
}
})
}
})
}
})
const getMakersInTournament = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getMakersInTournament', {
type: TournamentMakersResponse,
args: {
tournamentId: nonNull(intArg()),
...paginationArgs({ take: 10 }),
search: stringArg(),
roleId: intArg(),
openToConnect: booleanArg()
},
async resolve(_, args, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
let filters = [];
if (args.search) filters.push({
OR: [
{
name: {
contains: args.search,
mode: 'insensitive'
}
},
{
jobTitle: {
contains: args.search,
mode: 'insensitive'
}
}
]
})
if (args.roleId) filters.push({
roles: {
some: {
roleId: args.roleId
}
}
})
if (args.openToConnect) filters.push({
OR: [
{
github: {
not: ''
}
},
{
twitter: {
not: ''
}
},
{
linkedin: {
not: ''
}
},
]
})
if (user?.id) filters.push({
id: {
not: user.id
}
})
const makers = (await prisma.tournamentParticipant.findMany({
where: {
tournament_id: args.tournamentId,
...(filters.length > 0 && {
user: {
AND: filters
}
}),
...(args.openToConnect && {
hacking_status: TournamentMakerHackingStatusEnum.value.members.OpenToConnect
})
},
orderBy: {
createdAt: 'desc'
},
include: {
user: true,
},
skip: args.skip,
take: args.take + 1,
}))
.map(item => ({
hacking_status: item.hacking_status,
user: item.user
}))
return {
hasNext: makers.length === args.take + 1,
hasPrev: args.skip !== 0,
makers: makers.slice(0, args.take)
}
}
})
}
})
const getProjectsInTournament = extendType({
type: "Query",
definition(t) {
t.nonNull.field('getProjectsInTournament', {
type: TournamentProjectsResponse,
args: {
tournamentId: nonNull(intArg()),
...paginationArgs({ take: 10 }),
search: stringArg(),
roleId: intArg(),
},
async resolve(_, args) {
let filters = [];
if (args.search) filters.push({
OR: [
{
title: {
contains: args.search,
mode: 'insensitive'
}
},
{
description: {
contains: args.search,
mode: 'insensitive'
}
}
]
})
// if (args.roleId) filters.push({
// recruit_roles: {
// some: {
// roleId: args.roleId
// }
// }
// })
const projects = (await prisma.tournamentProject.findMany({
where: {
tournament_id: args.tournamentId,
...(filters.length > 0 && {
project: {
AND: filters
}
})
},
include: {
project: true,
},
skip: args.skip,
take: args.take + 1,
})).map(item => item.project)
console.log();
return {
hasNext: projects.length === args.take + 1,
hasPrev: args.skip !== 0,
projects: projects.slice(0, args.take)
}
}
})
}
})
const RegisterInTournamentInput = inputObjectType({
name: 'RegisterInTournamentInput',
definition(t) {
t.nonNull.string('email')
t.nonNull.field('hacking_status', { type: TournamentMakerHackingStatusEnum })
}
})
const registerInTournament = extendType({
type: 'Mutation',
definition(t) {
t.field('registerInTournament', {
type: 'User',
args: {
data: RegisterInTournamentInput,
tournament_id: nonNull(intArg())
},
async resolve(_root, { tournament_id, data: { email, hacking_status } }, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new Error("You have to login");
// Email verification here:
// ....
// ....
return (await prisma.tournamentParticipant.create({
data: {
tournament_id,
user_id: user.id,
email,
hacking_status
},
include: {
user: true
}
})).user;
}
})
},
})
const UpdateTournamentRegistrationInput = inputObjectType({
name: 'UpdateTournamentRegistrationInput',
definition(t) {
t.string('email')
t.field('hacking_status', { type: TournamentMakerHackingStatusEnum })
}
})
const updateTournamentRegistration = extendType({
type: 'Mutation',
definition(t) {
t.field('updateTournamentRegistration', {
type: ParticipationInfo,
args: {
data: UpdateTournamentRegistrationInput,
tournament_id: nonNull(intArg())
},
async resolve(_root, { tournament_id, data: { email, hacking_status } }, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
// if (!user)
// throw new Error("You have to login");
// Email verification here:
// ....
// ....
return prisma.tournamentParticipant.update({
where: {
tournament_id_user_id: { tournament_id, user_id: user.id }
},
data: removeNulls({
email,
hacking_status
}),
});
}
})
},
})
module.exports = {
// Types
Tournament,
// Enums
TournamentEventTypeEnum,
// Queries
getTournamentById,
getMakersInTournament,
getProjectsInTournament,
tournamentParticipationInfo,
getTournamentToRegister,
// Mutations
registerInTournament,
updateTournamentRegistration,
}

View File

@@ -1,577 +0,0 @@
const { prisma } = require('../../../prisma');
const { objectType, extendType, intArg, nonNull, inputObjectType, stringArg, interfaceType, list, enumType } = require("nexus");
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers");
const { ImageInput } = require('./misc');
const { Tournament } = require('./tournament');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { deleteImage } = require('../../../services/imageUpload.service');
const BaseUser = interfaceType({
name: 'BaseUser',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('name');
t.nonNull.string('avatar', {
async resolve(parent) {
return prisma.user.findUnique({ where: { id: parent.id } }).avatar_rel().then(resolveImgObjectToUrl)
}
});
t.nonNull.date('join_date');
t.string('role');
t.string('jobTitle')
t.string('lightning_address')
t.string('website')
t.string('twitter')
t.string('discord')
t.string('github')
t.string('linkedin')
t.string('bio')
t.string('location')
t.nonNull.list.nonNull.field('roles', {
type: MakerRole,
resolve: async (parent) => {
const data = await prisma.user.findUnique({
where: {
id: parent.id
},
select: {
roles: {
select: {
role: true,
level: true
}
},
}
})
return data.roles.map(data => {
return ({ ...data.role, level: data.level })
})
}
})
t.nonNull.list.nonNull.field('skills', {
type: MakerSkill,
resolve: (parent) => {
return prisma.user.findUnique({ where: { id: parent.id } }).skills();
}
})
t.nonNull.list.nonNull.field('tournaments', {
type: Tournament,
resolve: async (parent) => {
return prisma.tournamentParticipant.findMany({
where: {
user_id: parent.id
},
include: {
tournament: true
}
}).then(d => d.map(item => item.tournament))
}
})
t.nonNull.list.nonNull.field('projects', {
type: "Project",
resolve: async (parent) => {
return prisma.projectMember.findMany({
where: {
userId: parent.id
},
include: {
project: true
}
}).then(d => d.map(item => item.project))
}
})
t.nonNull.list.nonNull.field('similar_makers', {
type: "User",
resolve(parent,) {
return prisma.user.findMany({
where: {
AND: {
id: {
not: parent.id
}
}
},
take: 3,
})
}
})
t.nonNull.list.nonNull.field('stories', {
type: "Story",
resolve: (parent) => {
return prisma.story.findMany({ where: { user_id: parent.id, is_published: true }, orderBy: { createdAt: "desc" } });
}
});
t.nonNull.boolean('in_tournament', {
args: {
id: nonNull(intArg())
},
resolve(parent, args) {
return prisma.tournamentParticipant.findFirst({ where: { tournament_id: args.id, user_id: parent.id } }).then(res => !!res)
}
})
},
resolveType() {
return null
},
})
const RoleLevelEnum = enumType({
name: 'RoleLevelEnum',
members: {
Beginner: 0,
Hobbyist: 1,
Intermediate: 2,
Advanced: 3,
Pro: 4,
},
});
const GenericMakerRole = objectType({
name: 'GenericMakerRole',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('icon');
}
})
const MakerRole = objectType({
name: 'MakerRole',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('icon');
t.nonNull.field('level', { type: RoleLevelEnum })
}
})
const getAllMakersRoles = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getAllMakersRoles', {
type: GenericMakerRole,
async resolve(parent, args, context) {
return prisma.workRole.findMany();
}
})
}
})
const MakerSkill = objectType({
name: 'MakerSkill',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
}
})
const getAllMakersSkills = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('getAllMakersSkills', {
type: MakerSkill,
async resolve(parent, args, context) {
return prisma.skill.findMany();
}
})
}
})
const User = objectType({
name: 'User',
definition(t) {
t.implements('BaseUser')
}
})
const MyProfile = objectType({
name: 'MyProfile',
definition(t) {
t.implements('BaseUser')
t.string('email')
t.string('nostr_prv_key')
t.string('nostr_pub_key')
t.nonNull.list.nonNull.field('walletsKeys', {
type: "WalletKey",
resolve: (parent, _, context) => {
return prisma.userKey.findMany({
where: {
user_id: parent.id,
},
orderBy: {
createdAt: "asc"
}
})
.then(keys => keys.map(k => ({ ...k, is_current: k.key === context.userPubKey })))
}
});
}
})
const me = extendType({
type: "Query",
definition(t) {
t.field('me', {
type: "MyProfile",
async resolve(parent, args, context) {
const user = await getUserByPubKey(context.userPubKey)
return user
}
})
}
})
const profile = extendType({
type: "Query",
definition(t) {
t.field('profile', {
type: "User",
args: {
id: nonNull(intArg())
},
async resolve(parent, { id }, ctx) {
const user = await getUserByPubKey(ctx.userPubKey)
let isMy = false;
if (user?.id === id) isMy = true;
return prisma.user.findUnique({ where: { id } })
}
})
}
})
const searchUsers = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('searchUsers', {
type: "User",
args: {
value: nonNull(stringArg())
},
async resolve(_, { value }) {
return prisma.user.findMany({
where: {
name: {
contains: value,
mode: "insensitive"
}
},
})
}
})
}
})
const similarMakers = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('similarMakers', {
type: "User",
args: {
id: nonNull(intArg())
},
async resolve(parent, { id }, ctx) {
return prisma.user.findMany({
where: {
AND: {
id: {
not: id
}
}
},
take: 3,
})
}
})
}
})
const ProfileDetailsInput = inputObjectType({
name: 'ProfileDetailsInput',
definition(t) {
t.string('name');
t.field('avatar', {
type: ImageInput
})
t.string('email')
t.string('jobTitle')
t.string('lightning_address')
t.string('website')
t.string('twitter')
t.string('discord')
t.string('github')
t.string('linkedin')
t.string('bio')
t.string('location')
}
})
const updateProfileDetails = extendType({
type: 'Mutation',
definition(t) {
t.field('updateProfileDetails', {
type: 'MyProfile',
args: { data: ProfileDetailsInput },
async resolve(_root, args, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new Error("You have to login");
// TODO: validate new data
// ----------------
// Check if the user uploaded a new image, and if so,
// remove the old one from the hosting service, then replace it with this one
// ----------------
let avatarId = user.avatar_id;
if (args.data.avatar.id) {
const newAvatarProviderId = args.data.avatar.id;
const newAvatar = await prisma.hostedImage.findFirst({
where: {
provider_image_id: newAvatarProviderId
}
})
if (newAvatar && newAvatar.id !== user.avatar_id) {
avatarId = newAvatar.id;
await prisma.hostedImage.update({
where: {
id: newAvatar.id
},
data: {
is_used: true
}
});
await deleteImage(user.avatar_id)
}
}
// Preprocess & insert
return prisma.user.update({
where: {
id: user.id,
},
data: removeNulls({
...args.data,
avatar_id: avatarId,
//hack to remove avatar from args.data
// can be removed later with a schema data validator
avatar: '',
})
})
}
})
},
})
const WalletKey = objectType({
name: 'WalletKey',
definition(t) {
t.nonNull.string('key');
t.nonNull.string('name');
t.nonNull.date('createdAt');
t.nonNull.boolean('is_current')
}
})
const UserKeyInputType = inputObjectType({
name: 'UserKeyInputType',
definition(t) {
t.nonNull.string('key');
t.nonNull.string('name');
}
})
const updateUserPreferences = extendType({
type: 'Mutation',
definition(t) {
t.nonNull.field('updateUserPreferences', {
type: 'MyProfile',
args: { userKeys: list(nonNull(UserKeyInputType)) },
async resolve(_root, args, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
if (!user)
throw new Error("You have to login");
//Update the userkeys
//--------------------
// Check if all the sent keys belong to the user
const userKeys = (await prisma.userKey.findMany({
where: {
AND: {
user_id: {
equals: user.id,
},
key: {
in: args.userKeys.map(i => i.key)
}
},
},
select: {
key: true
}
})).map(i => i.key);
const newKeys = [];
for (let i = 0; i < args.userKeys.length; i++) {
const item = args.userKeys[i];
if (userKeys.includes(item.key))
newKeys.push(item);
}
if (newKeys.length === 0)
throw new Error("You can't delete all your wallets keys")
await prisma.userKey.deleteMany({
where: {
user_id: user.id
}
})
await prisma.userKey.createMany({
data: newKeys.map(i => ({
user_id: user.id,
key: i.key,
name: i.name,
}))
})
return prisma.user.findUnique({ where: { id: user.id } });
}
})
}
})
const MakerRoleInput = inputObjectType({
name: "MakerRoleInput",
definition(t) {
t.nonNull.int('id');
t.nonNull.field('level', { type: RoleLevelEnum })
}
})
const MakerSkillInput = inputObjectType({
name: "MakerSkillInput",
definition(t) {
t.nonNull.int('id');
}
})
const ProfileRolesInput = inputObjectType({
name: 'ProfileRolesInput',
definition(t) {
t.nonNull.list.nonNull.field('roles', {
type: MakerRoleInput,
})
t.nonNull.list.nonNull.field('skills', {
type: MakerSkillInput,
})
}
})
const updateProfileRoles = extendType({
type: 'Mutation',
definition(t) {
t.field('updateProfileRoles', {
type: 'MyProfile',
args: { data: ProfileRolesInput },
async resolve(_root, args, ctx) {
const user = await getUserByPubKey(ctx.userPubKey);
// Do some validation
if (!user)
throw new Error("You have to login");
await prisma.user.update({
where: {
id: user.id,
},
data: {
skills: {
set: [],
},
roles: {
deleteMany: {}
},
},
}
)
return prisma.user.update({
where: {
id: user.id,
},
data: {
skills: {
connect: args.data.skills,
},
roles: {
create: args.data.roles.map(r => ({ roleId: r.id, level: r.level }))
}
}
})
}
})
},
})
module.exports = {
// Types
BaseUser,
User,
MyProfile,
WalletKey,
MakerRole,
// Queries
me,
profile,
searchUsers,
similarMakers,
getAllMakersRoles,
getAllMakersSkills,
// Mutations
updateProfileDetails,
updateUserPreferences,
updateProfileRoles,
}

View File

@@ -1,219 +0,0 @@
const {
intArg,
objectType,
extendType,
nonNull,
stringArg,
arg,
enumType,
} = require('nexus')
const { parsePaymentRequest } = require('invoices');
const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers');
const { createHash } = require('crypto');
const { prisma } = require('../../../prisma');
const { CONSTS } = require('../../../utils');
// the types of items we can vote to
const VOTE_ITEM_TYPE = enumType({
name: 'VOTE_ITEM_TYPE',
members: ['Story', 'Bounty', 'Question', 'Project', 'User', 'PostComment'],
})
const Vote = objectType({
name: 'Vote',
definition(t) {
t.nonNull.int('id');
t.nonNull.int('amount_in_sat');
t.nonNull.string('payment_request');
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.nonNull.field('item_type', {
type: "VOTE_ITEM_TYPE"
})
t.nonNull.int('item_id');
}
})
const LnurlDetails = objectType({
name: 'LnurlDetails',
definition(t) {
t.int('minSendable');
t.int('maxSendable');
t.string('metadata');
t.int('commentAllowed');
}
})
const getModalOfType = (type) => {
switch (type) {
case "Story":
return prisma.story;
case "Question":
return prisma.question;
case "Project":
return prisma.project;
case "PostComment":
return prisma.postComment;
default:
return null;
}
}
const getLightningAddress = async (item_id, item_type) => {
switch (item_type) {
case "Story":
return prisma.story.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
case "Question":
return prisma.question.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
case "Project":
return prisma.project.findUnique({
where: { id: item_id },
select: {
lightning_address: true
}
}).then(data => data.lightning_address);
case "Comment":
return prisma.postComment.findUnique({
where: { id: item_id }, include: {
user: {
select: {
lightning_address: true
}
}
}
}).then(data => data.user.lightning_address);
default:
return null;
}
}
// type => modal
// type => lightning address (pr)
// This is the new voting mutation, it can vote for any type of item that we define in the VOTE_ITEM_TYPE enum
const voteMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('vote', {
type: "Vote",
args: {
item_type: arg({
type: nonNull("VOTE_ITEM_TYPE")
}),
item_id: nonNull(intArg()),
amount_in_sat: nonNull(intArg())
},
resolve: async (_, args) => {
const { item_id, item_type, amount_in_sat } = args;
const lightning_address = (await getLightningAddress(item_id, item_type)) ?? CONSTS.BOLT_FUN_LIGHTNING_ADDRESS;
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
const invoice = parsePaymentRequest({ request: pr });
// #TODO remove votes rows that get added but not confirmed after some time
// maybe using a scheduler, timeout, or whatever mean available
return prisma.vote.create({
data: {
item_type: item_type,
item_id: item_id,
amount_in_sat: amount_in_sat,
payment_request: pr,
payment_hash: invoice.id,
}
});
}
})
}
})
const confirmVoteMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('confirmVote', {
type: "Vote",
args: {
payment_request: nonNull(stringArg()),
preimage: nonNull(stringArg())
},
resolve: async (_, args) => {
const paymentHash = createHash("sha256")
.update(hexToUint8Array(args.preimage))
.digest("hex");
// look for a vote for the payment request and the calculated payment hash
const vote = await prisma.vote.findFirst({
where: {
payment_request: args.payment_request,
payment_hash: paymentHash,
},
});
// if we find a vote it means the preimage is correct and we update the vote and mark it as paid
// can we write this nicer?
if (vote) {
const modal = getModalOfType(vote.item_type);
const item = await modal.findUnique({
where: { id: vote.item_id },
});
// count up votes cache
await modal.update({
where: { id: item.id },
data: {
votes_count: item.votes_count + vote.amount_in_sat,
},
});
// return the current vote
return prisma.vote.update({
where: { id: vote.id },
data: {
paid: true,
preimage: args.preimage,
}
});
} else {
throw new Error("Invalid preimage");
}
}
})
}
})
module.exports = {
// Enums
VOTE_ITEM_TYPE,
// Types
Vote,
LnurlDetails,
// Mutations
voteMutation,
confirmVoteMutation
}

View File

@@ -1,72 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const lnurlAuthService = require('../../auth/services/lnurlAuth.service');
const isLoggedInHandler = async (req, res) => {
try {
const login_session = req.headers.session_token;
if (login_session) {
const { payload } = await jose.jwtVerify(login_session, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
});
const hash = payload.hash;
const authToken = await lnurlAuthService.getAuthTokenByHash(hash);
if (!authToken)
throw new Error("Not logged in yet")
lnurlAuthService.removeHash(hash).catch();
lnurlAuthService.removeExpiredHashes().catch();
res
.status(200)
.clearCookie('login_session', {
secure: true,
httpOnly: true,
sameSite: "none",
})
.cookie('Authorization', authToken, {
maxAge: 3600000 * 24 * 30,
secure: true,
httpOnly: true,
sameSite: "none",
})
.json({
logged_in: true
});
} else {
res.json({
logged_in: false
});
}
} catch (error) {
res.json({
logged_in: false
})
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.get('/is-logged-in', isLoggedInHandler);
}
else {
const router = express.Router();
router.get('/is-logged-in', (isLoggedInHandler))
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,170 +0,0 @@
const { prisma } = require('../../prisma');
const LnurlAuthService = require('../../auth/services/lnurlAuth.service')
const serverless = require('serverless-http');
const { createHash, associateTokenToHash } = require('../../auth/services/lnurlAuth.service');
const { createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const { generatePrivateKey, getPublicKey } = require('../../utils/nostr-tools');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const { logError } = require('../../utils/logger');
const loginHandler = async (req, res) => {
const { tag, k1, sig, key, action, user_token } = req.query;
if (tag !== 'login')
return res.status(400).json({ status: 'ERROR', reason: 'Invalid Tag Provided' })
// Verify login params
try {
await LnurlAuthService.verifySig(sig, k1, key)
} catch (error) {
return res.status(400).json({ status: 'ERROR', reason: 'Invalid Signature' })
}
if (action === 'link' && user_token) {
try {
let user_id;
try {
const { payload } = await jose.jwtVerify(user_token, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
})
user_id = payload.user_id;
} catch (error) {
return res.status(400).json({ status: 'ERROR', reason: "Invalid user_token" })
}
const existingKeys = await prisma.userKey.findMany({ where: { user_id }, select: { key: true } });
if (existingKeys.length >= 3)
return res.status(400).json({ status: 'ERROR', reason: "Can only link up to 3 wallets" })
// Remove old linking for this key if existing
await prisma.userKey.deleteMany({
where: { key }
})
await prisma.userKey.create({
data: {
key,
user_id,
}
});
return res
.status(200)
.json({ status: "OK" })
} catch (error) {
logError(error)
return res.status(500).json({ status: 'ERROR', reason: 'Unexpected error happened' })
}
}
try {
//Create user if not already existing
const user = await getUserByPubKey(key)
if (user === null) {
// Check if user had a previous account using this wallet
const oldAccount = await prisma.user.findFirst({
where: {
pubKey: key
}
});
if (oldAccount) {
await prisma.userKey.create({
data: {
key,
name: "My original wallet key",
user_id: oldAccount.id,
}
});
} else {
const nostr_prv_key = generatePrivateKey();
const nostr_pub_key = getPublicKey(nostr_prv_key);
const avatar = await prisma.hostedImage.create({
data: {
filename: 'avatar.svg',
provider: 'external',
is_used: true,
url: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
provider_image_id: ''
}
})
const createdUser = await prisma.user.create({
data: {
pubKey: key,
name: key,
avatar_id: avatar.id,
nostr_prv_key,
nostr_pub_key,
},
})
await prisma.userKey.create({
data: {
key,
name: "My original wallet key",
user_id: createdUser.id,
}
});
}
}
// calc the hash of k1
const hash = createHash(k1);
// generate the auth jwt token
const hour = 3600000
const maxAge = 30 * 24 * hour;
const authToken = await new jose.SignJWT({ pubKey: key })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(maxAge)
//TODO: Set audience, issuer
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
// associate the auth token with the hash in the db
await associateTokenToHash(hash, authToken);
return res.status(200).json({ status: "OK" })
} catch (error) {
logError(error)
return res.status(500).json({ status: 'ERROR', reason: 'Unexpected error happened, please try again' })
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.get('/login', loginHandler);
}
else {
const router = express.Router();
router.get('/login', loginHandler)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,34 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const logoutHandler = (req, res, next) => {
res
.clearCookie('Authorization', {
secure: true,
httpOnly: true,
sameSite: "none",
})
.redirect("/")
.end()
};
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.get('/logout', logoutHandler);
}
else {
const router = express.Router();
router.get('/logout', logoutHandler)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,97 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const { verifySignature, validateEvent } = require('../../utils/nostr-tools');
const { prisma } = require('../../prisma');
const signEvent = async (req, res) => {
try {
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey);
if (!user)
return res.status(401).json({ status: 'ERROR', reason: 'Not Authenticated' });
const pubkey = user.nostr_pub_key;
const event = req.body.event
if (!validateEvent(event))
return res.status(400).send("Event not valid");
if (event.pubkey !== pubkey || !(await verifySignature(event)))
return res.status(400).send("Signature not valid")
// Extract type & id
const rTag = event.tags.find(tag => tag[0] === 'r');
const [host, type, refId] = rTag[1].split(' ');
if (host !== 'boltfun') return res.status(400).send("This event wasn't signed by bolt.fun");
if (type === 'Story_comment') {
// Extract replyTo id
const eTag = event.tags.find(tag => tag[0] === 'e');
let parent_comment_id;
if (eTag) {
const [parent_nostr_comment_id] = eTag[1].split(' ');
if (parent_nostr_comment_id)
parent_comment_id = (await prisma.postComment.findFirst({
where: {
nostr_id: parent_nostr_comment_id
}
}))?.id;
}
// Insert comment in database
await prisma.postComment.create({
data: {
nostr_id: event.id,
body: event.content,
story_id: Number(refId),
user_id: user.id,
parent_comment_id
},
})
}
return res
.status(200)
.end()
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/nostr-confirm-event', signEvent);
}
else {
const router = express.Router();
router.post('/nostr-confirm-event', signEvent)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,66 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const { prisma } = require('../../prisma');
const getEventsExtraData = async (req, res) => {
try {
const ids = req.body.ids ?? []
const comments = await prisma.postComment.findMany({
where: {
nostr_id: {
in: ids
}
},
select: {
id: true,
nostr_id: true,
votes_count: true,
user: {
select: {
id: true,
avatar: true,
avatar_rel: {
select: {
url: true,
}
},
name: true,
}
}
}
});
comments.map(c => c.user.avatar = c.user.avatar_rel?.url ?? c.user.avatar)
return res
.status(200)
.json(comments)
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/nostr-events-extra-data', getEventsExtraData);
}
else {
const router = express.Router();
router.post('/nostr-events-extra-data', getEventsExtraData)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,74 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const { generatePrivateKey, getPublicKey, signEvent: signNostrEvent } = require('../../utils/nostr-tools');
const { prisma } = require('../../prisma');
const signEvent = async (req, res) => {
try {
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey);
if (!user)
return res.status(401).json({ status: 'ERROR', reason: 'Not Authenticated' });
let prvkey = user.nostr_prv_key, pubkey = user.nostr_pub_key;
if (!prvkey) {
prvkey = generatePrivateKey();
pubkey = getPublicKey(prvkey);
await prisma.user.update({
where: {
id: user.id,
},
data: {
nostr_prv_key: prvkey,
nostr_pub_key: pubkey
}
})
}
const { content, tags, kind = 1 } = req.body.event
const event = {
kind,
pubkey,
content,
tags,
created_at: Math.round(Date.now() / 1000),
}
event.sig = await signNostrEvent(event, prvkey);
return res
.status(200)
.json({ event });
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/nostr-sign-event', signEvent);
}
else {
const router = express.Router();
router.post('/nostr-sign-event', signEvent)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,62 +0,0 @@
const serverless = require('serverless-http');
const { createExpressApp } = require('../../modules');
const express = require('express');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const { prisma } = require('../../prisma');
const mapPubkeysToUsers = async (req, res) => {
try {
const pubkeys = req.body.pubkeys ?? [];
const usersArr = await prisma.user.findMany({
where: {
nostr_pub_key: {
in: pubkeys
}
},
select: {
id: true,
name: true,
avatar: true,
nostr_pub_key: true,
lightning_address: true
}
})
let pubkeysToUsers = {}
usersArr.forEach(user => {
pubkeysToUsers[user.nostr_pub_key] = user;
});
return res
.status(200)
.json({ pubkeysToUsers });
} catch (error) {
console.log(error);
res.status(500).send("Unexpected error happened, please try again")
}
}
let app;
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/pubkeys-to-users', mapPubkeysToUsers);
}
else {
const router = express.Router();
router.post('/pubkeys-to-users', mapPubkeysToUsers)
app = createExpressApp(router)
}
const handler = serverless(app);
exports.handler = async (event, context) => {
return await handler(event, context);
};

View File

@@ -1,53 +0,0 @@
const serverless = require('serverless-http')
const { createExpressApp } = require('../../modules')
const express = require('express')
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie')
const { getUserByPubKey } = require('../../auth/utils/helperFuncs')
const { getDirectUploadUrl } = require('../../services/imageUpload.service')
const { prisma } = require('../../prisma')
const { getUrlFromProvider } = require('../../utils/resolveImageUrl')
const postUploadImageUrl = async (req, res) => {
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey)
if (!user) return res.status(401).json({ status: 'ERROR', reason: 'Not Authenticated' })
const { filename } = req.body
if (!filename) return res.status(422).json({ status: 'ERROR', reason: "The field 'filename' is required`" })
try {
const uploadUrl = await getDirectUploadUrl()
const hostedImage = await prisma.hostedImage.create({
data: {
filename,
url: getUrlFromProvider(uploadUrl.provider, uploadUrl.id),
provider_image_id: uploadUrl.id,
provider: uploadUrl.provider
},
})
return res.status(200).json({ id: hostedImage.id, uploadURL: uploadUrl.uploadURL })
} catch (error) {
res.status(500).send('Unexpected error happened, please try again')
}
}
let app
if (process.env.LOCAL) {
app = createExpressApp()
app.post('/upload-image-url', postUploadImageUrl)
} else {
const router = express.Router()
router.post('/upload-image-url', postUploadImageUrl)
app = createExpressApp(router)
}
const handler = serverless(app)
exports.handler = async (event, context) => {
return await handler(event, context)
}

View File

@@ -1,32 +0,0 @@
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser')
const createExpressApp = (router) => {
const app = express();
const routerBasePath = process.env.LOCAL ? `/dev` : `/.netlify/functions`
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())
app.use(cookieParser());
app.use(cors({
origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
credentials: true,
}))
if (router)
app.use(routerBasePath, router);
return app;
}
module.exports = createExpressApp;

View File

@@ -1,6 +0,0 @@
const createExpressApp = require("./express-app");
module.exports = {
createExpressApp,
}

View File

@@ -1,3 +0,0 @@
import { PrismaClient } from '@prisma/client'
export const prisma: PrismaClient;

View File

@@ -1,19 +0,0 @@
const { PrismaClient } = process.env.PRISMA_GENERATE_DATAPROXY ? require('@prisma/client/edge') : require('@prisma/client');
const createGlobalModule = require('../utils/createGlobalModule');
const createPrismaClient = () => {
console.log("New Prisma Client");
try {
return new PrismaClient();
} catch (error) {
console.log(error);
}
}
const prisma = createGlobalModule('prisma', createPrismaClient)
module.exports = {
prisma
}

View File

@@ -1,98 +0,0 @@
const { CONSTS } = require('../utils')
const axios = require('axios')
const FormData = require('form-data')
const { prisma } = require('../prisma')
const BASE_URL = 'https://api.cloudflare.com/client/v4'
const operationUrls = {
'image.uploadUrl': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v2/direct_upload`,
'image.delete': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v1/`,
}
function getAxiosConfig() {
return {
headers: {
Authorization: `Bearer ${CONSTS.CLOUDFLARE_IMAGE_API_KEY}`,
},
}
}
async function getDirectUploadUrl() {
const url = operationUrls['image.uploadUrl']
const formData = new FormData()
formData.append('requireSignedURLs', 'false')
const config = {
headers: {
Authorization: `Bearer ${CONSTS.CLOUDFLARE_IMAGE_API_KEY}`,
...formData.getHeaders(),
},
}
const result = await axios.post(url, formData, config)
if (!result.data.success) {
throw new Error(result.data, { cause: result.data.errors })
}
const data = result.data.result
return { id: data.id, uploadURL: data.uploadURL, provider: 'cloudflare' }
}
async function deleteImageFromProvider(providerImageId) {
try {
const url = operationUrls['image.delete'] + providerImageId
const result = await axios.delete(url, getAxiosConfig())
if (!result.data.success) {
throw new Error(result.data, { cause: result.data.errors })
}
} catch (error) {
throw error
}
}
async function deleteImage(hostedImageId) {
if (!hostedImageId) throw new Error("argument 'hostedImageId' must be provider")
const hostedImage = await prisma.hostedImage.findFirst({
where: {
id: hostedImageId,
},
})
if (!hostedImage) throw new Error(`No HostedImage row found for HostedImage.id=${hostedImageId}`)
if (hostedImage.provider_image_id && hostedImage.provider_image_id === '')
throw new Error(`Field 'provider_image_id' for HostedImage.id=${hostedImageId} must not be empty. Current value '${hostedImage.provider_image_id}'`)
// Set is_used to false in case of deletion fail from the hosting image provider. The scheduled job will try to delete the HostedImage row
await prisma.hostedImage.update({
where: {
id: hostedImage.id,
},
data: {
is_used: false,
},
})
if (hostedImage.provider_image_id && hostedImage.provider_image_id !== '') {
deleteImageFromProvider(hostedImage.provider_image_id)
.then(async () => {
await prisma.hostedImage.delete({
where: {
id: hostedImageId,
},
})
})
.catch((error) => console.error(error))
}
}
module.exports = {
getDirectUploadUrl,
deleteImage,
deleteImageFromProvider,
}

View File

@@ -1,17 +0,0 @@
const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com' // #TODO, replace it by bolt-fun lightning address if there exist one
const JWT_SECRET = process.env.JWT_SECRET
const LNURL_AUTH_HOST = process.env.LNURL_AUTH_HOST
const CLOUDFLARE_IMAGE_ACCOUNT_ID = process.env.CLOUDFLARE_IMAGE_ACCOUNT_ID
const CLOUDFLARE_IMAGE_API_KEY = process.env.CLOUDFLARE_IMAGE_API_KEY
const CLOUDFLARE_IMAGE_ACCOUNT_HASH = process.env.CLOUDFLARE_IMAGE_ACCOUNT_HASH
const CONSTS = {
JWT_SECRET,
BOLT_FUN_LIGHTNING_ADDRESS,
LNURL_AUTH_HOST,
CLOUDFLARE_IMAGE_ACCOUNT_ID,
CLOUDFLARE_IMAGE_API_KEY,
CLOUDFLARE_IMAGE_ACCOUNT_HASH
}
module.exports = CONSTS

View File

@@ -1,9 +0,0 @@
const createGlobalModule = (name, factoryFn) => {
if (!global[name]) {
global[name] = factoryFn();
}
return global[name];
}
module.exports = createGlobalModule

View File

@@ -1,22 +0,0 @@
const cookie = require('cookie')
const jose = require('jose');
const { JWT_SECRET } = require("./consts");
const extractKeyFromCookie = async (cookieHeader) => {
const cookies = cookie.parse(cookieHeader ?? '');
const token = cookies.Authorization;
if (token) {
try {
const { payload } = await jose.jwtVerify(token, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
})
return payload.pubKey
} catch (error) {
return null
}
}
return null;
}
module.exports = extractKeyFromCookie;

View File

@@ -1,6 +0,0 @@
const crypto = require('crypto');
const generateId = () => crypto.randomUUID({});
module.exports = generateId;

View File

@@ -1,5 +0,0 @@
module.exports = {
CONSTS: require('./consts'),
nostr_tools: require('./nostr-tools')
}

View File

@@ -1,9 +0,0 @@
function logError(error) {
console.log("Unexpected Error: ");
console.log(error);
}
module.exports = {
logError
}

View File

@@ -1,69 +0,0 @@
// import * as secp256k1 from '@noble/secp256k1'
// import { Buffer } from 'buffer'
const secp256k1 = require('@noble/secp256k1');
const crypto = require('crypto')
function generatePrivateKey() {
return Buffer.from(secp256k1.utils.randomPrivateKey()).toString('hex')
}
function getPublicKey(privateKey) {
return Buffer.from(secp256k1.schnorr.getPublicKey(privateKey)).toString('hex')
}
function serializeEvent(evt) {
return JSON.stringify([
0,
evt.pubkey,
evt.created_at,
evt.kind,
evt.tags,
evt.content
])
}
function getEventHash(event) {
let eventHash = crypto.createHash('sha256')
.update(Buffer.from(serializeEvent(event)))
.digest()
return Buffer.from(eventHash).toString('hex')
}
async function signEvent(event, key) {
return Buffer.from(
await secp256k1.schnorr.sign(getEventHash(event), key)
).toString('hex')
}
function validateEvent(event) {
if (event.id !== getEventHash(event)) return false
if (typeof event.content !== 'string') return false
if (typeof event.created_at !== 'number') return false
if (!Array.isArray(event.tags)) return false
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i]
if (!Array.isArray(tag)) return false
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === 'object') return false
}
}
return true
}
function verifySignature(event) {
return secp256k1.schnorr.verify(event.sig, event.id, event.pubkey)
}
module.exports = {
generatePrivateKey,
getPublicKey,
signEvent,
validateEvent,
verifySignature,
}

View File

@@ -1,45 +0,0 @@
const { CLOUDFLARE_IMAGE_ACCOUNT_HASH } = require('./consts')
const PROVIDERS = [
{
name: 'cloudflare',
prefixUrl: `https://imagedelivery.net/${CLOUDFLARE_IMAGE_ACCOUNT_HASH}/`,
variants: [
{
default: true,
name: 'public',
},
],
},
]
/**
* resolveImgObjectToUrl
* @param {object} imgObject
* @param {string} variant - List to be defined. DEFAULT TO 'public'
* @returns {string} image url
*/
function resolveImgObjectToUrl(imgObject, variant = null) {
if (!imgObject) return null;
if (imgObject.provider === 'external') {
return imgObject.url
}
return getUrlFromProvider(imgObject.provider, imgObject.provider_image_id, variant)
}
function getUrlFromProvider(provider, providerImageId, variant = null) {
const p = PROVIDERS.find((p) => p.name === provider)
if (p) {
if (p && p.name === 'cloudflare') {
const variantName = variant ?? p.variants.find((v) => v.default).name
return p.prefixUrl + providerImageId + '/' + variantName
}
}
throw new Error('Hosting images provider not supported')
}
module.exports = { resolveImgObjectToUrl, getUrlFromProvider }

View File

@@ -1,11 +1,7 @@
[build]
functions = "api/functions" # netlify-lambda builds to this folder AND Netlify reads functions from here
[dev]
framework = "#static"
functions = "api/functions" # netlify-lambda builds to this folder AND Netlify reads functions from here
publish = "build" # create-react-app builds to this folder, Netlify should serve all these files statically

View File

@@ -1,5 +1,5 @@
{
"name": "makers-bolt-fun",
"name": "lightning-landscape",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -99,31 +99,14 @@
},
"scripts": {
"client:dev-server": "env-cmd -f ./envs/client/dev-server.env react-scripts start",
"server:dev": "env-cmd -f ./envs/server/local.env serverless offline",
"client:preview-server": "env-cmd -f ./envs/client/preview-server.env react-scripts start",
"server:preview": "env-cmd -f ./envs/server/preview.env serverless offline",
"client:prod-server": "env-cmd -f ./envs/client/prod-server.env react-scripts start",
"server:prod": "env-cmd -f ./envs/server/prod.env serverless offline",
"client:mocks": "env-cmd -f ./envs/client/mock-server.env react-scripts start",
"generate-graphql": "graphql-codegen",
"storybook": "env-cmd -f ./envs/client/preview-server.env start-storybook -p 6006 -s public",
"storybook:mocks": "env-cmd -f ./envs/client/mock-server.env start-storybook -p 6006 -s public",
"build": "react-scripts build",
"build:mocks": "env-cmd -f ./envs/client/mock-server.env react-scripts build",
"build-storybook": "env-cmd -f ./envs/client/preview-server.env build-storybook -s public",
"build-storybook:mocks": "env-cmd -f ./envs/client/mock-server.env build-storybook -s public",
"test": "react-scripts test",
"eject": "react-scripts eject",
"db:migrate-dev": "prisma migrate dev",
"db:migrate-deploy": "prisma migrate deploy",
"db:reset": "prisma migrate reset",
"db:seed": "prisma db seed",
"db:gui": "prisma studio",
"netlify:start": "set NETLIFY=true&& netlify dev"
},
"prisma": {
"seed": "node prisma/seed/index.js"
},
"eslintConfig": {
"extends": [
"react-app",

View File

@@ -1,41 +0,0 @@
-- CreateTable
CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Project" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"website" TEXT NOT NULL,
"thumbnail_image" TEXT,
"cover_image" TEXT,
"category_id" INTEGER NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"lightning_address" TEXT,
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Vote" (
"id" SERIAL NOT NULL,
"project_id" INTEGER NOT NULL,
"amount_in_sat" INTEGER NOT NULL,
"payment_request" TEXT,
"payment_hash" TEXT,
"preimage" TEXT,
"paid" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Vote_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "lnurl_callback_url" TEXT;

View File

@@ -1,20 +0,0 @@
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "cover_image" TEXT,
ADD COLUMN "icon" TEXT;
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "screenshots" TEXT[];
-- CreateTable
CREATE TABLE "Award" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"project_id" INTEGER NOT NULL,
CONSTRAINT "Award_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Award" ADD CONSTRAINT "Award_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- You are about to drop the column `cover_image` on the `Award` table. All the data in the column will be lost.
- You are about to drop the column `icon` on the `Award` table. All the data in the column will be lost.
- Added the required column `image` to the `Award` table without a default value. This is not possible if the table is not empty.
- Added the required column `url` to the `Award` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Award" DROP COLUMN "cover_image",
DROP COLUMN "icon",
ADD COLUMN "image" TEXT NOT NULL,
ADD COLUMN "url" TEXT NOT NULL;

View File

@@ -1,25 +0,0 @@
-- CreateTable
CREATE TABLE "Tag" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ProjectToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_ProjectToTag_AB_unique" ON "_ProjectToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_ProjectToTag_B_index" ON "_ProjectToTag"("B");
-- AddForeignKey
ALTER TABLE "_ProjectToTag" ADD FOREIGN KEY ("A") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProjectToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[title]` on the table `Tag` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Tag_title_key" ON "Tag"("title");

View File

@@ -1,159 +0,0 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"username" TEXT NOT NULL,
"lightning_address" TEXT,
"avatar" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Story" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"body" TEXT NOT NULL,
"thumbnail_image" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"topic_id" INTEGER NOT NULL,
"user_id" INTEGER,
CONSTRAINT "Story_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Question" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"body" TEXT NOT NULL,
"thumbnail_image" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"topic_id" INTEGER NOT NULL,
"user_id" INTEGER,
CONSTRAINT "Question_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Topic" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"icon" TEXT NOT NULL,
CONSTRAINT "Topic_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PostComment" (
"id" SERIAL NOT NULL,
"body" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"parent_comment_id" INTEGER,
"user_id" INTEGER,
"story_id" INTEGER,
"question_id" INTEGER,
CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Hackathon" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"description" TEXT NOT NULL,
"location" TEXT NOT NULL,
"website" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Hackathon_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_StoryToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_QuestionToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_HackathonToTopic" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Topic_title_key" ON "Topic"("title");
-- CreateIndex
CREATE UNIQUE INDEX "_StoryToTag_AB_unique" ON "_StoryToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_StoryToTag_B_index" ON "_StoryToTag"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_QuestionToTag_AB_unique" ON "_QuestionToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_QuestionToTag_B_index" ON "_QuestionToTag"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_HackathonToTopic_AB_unique" ON "_HackathonToTopic"("A", "B");
-- CreateIndex
CREATE INDEX "_HackathonToTopic_B_index" ON "_HackathonToTopic"("B");
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_topic_id_fkey" FOREIGN KEY ("topic_id") REFERENCES "Topic"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Question" ADD CONSTRAINT "Question_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Question" ADD CONSTRAINT "Question_topic_id_fkey" FOREIGN KEY ("topic_id") REFERENCES "Topic"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_story_id_fkey" FOREIGN KEY ("story_id") REFERENCES "Story"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_question_id_fkey" FOREIGN KEY ("question_id") REFERENCES "Question"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_parent_comment_id_fkey" FOREIGN KEY ("parent_comment_id") REFERENCES "PostComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StoryToTag" ADD FOREIGN KEY ("A") REFERENCES "Story"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StoryToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_QuestionToTag" ADD FOREIGN KEY ("A") REFERENCES "Question"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_QuestionToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_HackathonToTopic" ADD FOREIGN KEY ("A") REFERENCES "Hackathon"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_HackathonToTopic" ADD FOREIGN KEY ("B") REFERENCES "Topic"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,23 +0,0 @@
/*
Warnings:
- You are about to drop the column `created_at` on the `PostComment` table. All the data in the column will be lost.
- You are about to drop the column `date` on the `Question` table. All the data in the column will be lost.
- You are about to drop the column `date` on the `Story` table. All the data in the column will be lost.
- Added the required column `updatedAt` to the `Question` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Story` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PostComment" DROP COLUMN "created_at",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Question" DROP COLUMN "date",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- AlterTable
ALTER TABLE "Story" DROP COLUMN "date",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

View File

@@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the column `date` on the `Hackathon` table. All the data in the column will be lost.
- You are about to drop the column `thumbnail_image` on the `Question` table. All the data in the column will be lost.
- You are about to drop the column `thumbnail_image` on the `Story` table. All the data in the column will be lost.
- Added the required column `end_date` to the `Hackathon` table without a default value. This is not possible if the table is not empty.
- Added the required column `start_date` to the `Hackathon` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Hackathon" DROP COLUMN "date",
ADD COLUMN "end_date" DATE NOT NULL,
ADD COLUMN "start_date" DATE NOT NULL;
-- AlterTable
ALTER TABLE "Question" DROP COLUMN "thumbnail_image";
-- AlterTable
ALTER TABLE "Story" DROP COLUMN "thumbnail_image";

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `created_at` on the `Project` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Project" DROP COLUMN "created_at",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,17 +0,0 @@
/*
Warnings:
- You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
- A unique constraint covering the columns `[name]` on the table `User` will be added. If there are existing duplicate values, this will fail.
- Added the required column `name` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "User_username_key";
-- AlterTable
ALTER TABLE "User" DROP COLUMN "username",
ADD COLUMN "name" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `excerpt` to the `Question` table without a default value. This is not possible if the table is not empty.
- Added the required column `excerpt` to the `Story` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Question" ADD COLUMN "excerpt" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "excerpt" TEXT NOT NULL;

View File

@@ -1,15 +0,0 @@
/*
Warnings:
- You are about to drop the column `project_id` on the `Vote` table. All the data in the column will be lost.
- Added the required column `item_id` to the `Vote` table without a default value. This is not possible if the table is not empty.
- Added the required column `item_type` to the `Vote` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Vote" DROP CONSTRAINT "Vote_project_id_fkey";
-- AlterTable
ALTER TABLE "Vote" DROP COLUMN "project_id",
ADD COLUMN "item_id" INTEGER NOT NULL,
ADD COLUMN "item_type" TEXT NOT NULL;

View File

@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "Donation" (
"id" SERIAL NOT NULL,
"amount" INTEGER NOT NULL,
"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"payment_request" TEXT,
"payment_hash" TEXT,
"preimage" TEXT,
"paid" BOOLEAN NOT NULL DEFAULT false,
"donor_id" INTEGER,
CONSTRAINT "Donation_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Donation" ADD CONSTRAINT "Donation_donor_id_fkey" FOREIGN KEY ("donor_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,23 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[pubKey]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "email" TEXT,
ADD COLUMN "pubKey" TEXT,
ADD COLUMN "website" TEXT,
ALTER COLUMN "avatar" DROP NOT NULL,
ALTER COLUMN "name" DROP NOT NULL;
-- CreateTable
CREATE TABLE "GeneratedK1" (
"value" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GeneratedK1_pkey" PRIMARY KEY ("value")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_pubKey_key" ON "User"("pubKey");

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "join_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "bio" TEXT,
ADD COLUMN "github" TEXT,
ADD COLUMN "location" TEXT,
ADD COLUMN "role" TEXT NOT NULL DEFAULT E'user',
ADD COLUMN "twitter" TEXT;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "jobTitle" TEXT,
ADD COLUMN "linkedin" TEXT;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "GeneratedK1" ADD COLUMN "sid" TEXT;

View File

@@ -1,46 +0,0 @@
/*
Warnings:
- You are about to drop the `Topic` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_HackathonToTopic` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Question" DROP CONSTRAINT "Question_topic_id_fkey";
-- DropForeignKey
ALTER TABLE "Story" DROP CONSTRAINT "Story_topic_id_fkey";
-- DropForeignKey
ALTER TABLE "_HackathonToTopic" DROP CONSTRAINT "_HackathonToTopic_A_fkey";
-- DropForeignKey
ALTER TABLE "_HackathonToTopic" DROP CONSTRAINT "_HackathonToTopic_B_fkey";
-- AlterTable
ALTER TABLE "Tag" ADD COLUMN "icon" TEXT,
ADD COLUMN "isOfficial" BOOLEAN NOT NULL DEFAULT false;
-- DropTable
DROP TABLE "Topic";
-- DropTable
DROP TABLE "_HackathonToTopic";
-- CreateTable
CREATE TABLE "_HackathonToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_HackathonToTag_AB_unique" ON "_HackathonToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_HackathonToTag_B_index" ON "_HackathonToTag"("B");
-- AddForeignKey
ALTER TABLE "_HackathonToTag" ADD FOREIGN KEY ("A") REFERENCES "Hackathon"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_HackathonToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `topic_id` on the `Question` table. All the data in the column will be lost.
- You are about to drop the column `topic_id` on the `Story` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Question" DROP COLUMN "topic_id";
-- AlterTable
ALTER TABLE "Story" DROP COLUMN "topic_id";

View File

@@ -1,2 +0,0 @@
-- DropIndex
DROP INDEX "User_name_key";

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Story" ALTER COLUMN "cover_image" DROP NOT NULL;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Tag" ADD COLUMN "description" TEXT;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "is_published" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Story" ALTER COLUMN "is_published" SET DEFAULT true;

View File

@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "Question" ADD COLUMN "is_published" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "nostr_prv_key" TEXT,
ADD COLUMN "nostr_pub_key" TEXT;

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `createdAt` on the `PostComment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "PostComment" DROP COLUMN "createdAt",
ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,19 +0,0 @@
/*
Warnings:
- The primary key for the `PostComment` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- DropForeignKey
ALTER TABLE "PostComment" DROP CONSTRAINT "PostComment_parent_comment_id_fkey";
-- AlterTable
ALTER TABLE "PostComment" DROP CONSTRAINT "PostComment_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "parent_comment_id" SET DATA TYPE TEXT,
ADD CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "PostComment_id_seq";
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_parent_comment_id_fkey" FOREIGN KEY ("parent_comment_id") REFERENCES "PostComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,22 +0,0 @@
/*
Warnings:
- The primary key for the `PostComment` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `PostComment` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `parent_comment_id` column on the `PostComment` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- DropForeignKey
ALTER TABLE "PostComment" DROP CONSTRAINT "PostComment_parent_comment_id_fkey";
-- AlterTable
ALTER TABLE "PostComment" DROP CONSTRAINT "PostComment_pkey",
ADD COLUMN "nostr_id" TEXT,
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
DROP COLUMN "parent_comment_id",
ADD COLUMN "parent_comment_id" INTEGER,
ADD CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id");
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_parent_comment_id_fkey" FOREIGN KEY ("parent_comment_id") REFERENCES "PostComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,10 +0,0 @@
-- CreateTable
CREATE TABLE "UserKey" (
"key" TEXT NOT NULL,
"user_id" INTEGER,
CONSTRAINT "UserKey_pkey" PRIMARY KEY ("key")
);
-- AddForeignKey
ALTER TABLE "UserKey" ADD CONSTRAINT "UserKey_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "UserKey" ADD COLUMN "name" TEXT NOT NULL DEFAULT E'New Key Name';

View File

@@ -1,57 +0,0 @@
-- AlterTable
ALTER TABLE "UserKey" ALTER COLUMN "name" SET DEFAULT E'My new wallet key';
-- CreateTable
CREATE TABLE "UsersOnWorkRoles" (
"userId" INTEGER NOT NULL,
"roleId" INTEGER NOT NULL,
CONSTRAINT "UsersOnWorkRoles_pkey" PRIMARY KEY ("userId","roleId")
);
-- CreateTable
CREATE TABLE "WorkRole" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"icon" TEXT NOT NULL,
CONSTRAINT "WorkRole_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Skill" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Skill_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_SkillToUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "WorkRole_title_key" ON "WorkRole"("title");
-- CreateIndex
CREATE UNIQUE INDEX "Skill_title_key" ON "Skill"("title");
-- CreateIndex
CREATE UNIQUE INDEX "_SkillToUser_AB_unique" ON "_SkillToUser"("A", "B");
-- CreateIndex
CREATE INDEX "_SkillToUser_B_index" ON "_SkillToUser"("B");
-- AddForeignKey
ALTER TABLE "UsersOnWorkRoles" ADD CONSTRAINT "UsersOnWorkRoles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UsersOnWorkRoles" ADD CONSTRAINT "UsersOnWorkRoles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "WorkRole"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_SkillToUser" ADD FOREIGN KEY ("A") REFERENCES "Skill"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_SkillToUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `level` to the `UsersOnWorkRoles` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "UsersOnWorkRoles" ADD COLUMN "level" TEXT NOT NULL;

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- Changed the type of `level` on the `UsersOnWorkRoles` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- AlterTable
ALTER TABLE "UsersOnWorkRoles" DROP COLUMN "level",
ADD COLUMN "level" INTEGER NOT NULL;

View File

@@ -1,9 +0,0 @@
-- CreateTable
CREATE TABLE "HostedImage" (
"id" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"is_used" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "HostedImage_pkey" PRIMARY KEY ("id")
);

View File

@@ -1,29 +0,0 @@
/*
Warnings:
- The primary key for the `HostedImage` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `HostedImage` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- Added the required column `provider` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
- Added the required column `provider_image_id` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
- Added the required column `url` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
*/
-- START Custom SQL
-- Because of the breaking changes, we have to apply some custom SQL.
-- By chance, the previous HostedImage migration is not used it production, so we can remove all data from this table
DELETE FROM "HostedImage";
-- END Custom SQL
-- AlterTable
ALTER TABLE "HostedImage" DROP CONSTRAINT "HostedImage_pkey",
ADD COLUMN "provider" TEXT NOT NULL,
ADD COLUMN "provider_image_id" TEXT NOT NULL,
ADD COLUMN "url" TEXT NOT NULL,
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "HostedImage_pkey" PRIMARY KEY ("id");
-- AlterTable
ALTER TABLE "UserKey" ALTER COLUMN "name" SET DEFAULT E'My new wallet key';

View File

@@ -1,14 +0,0 @@
-- CreateTable
CREATE TABLE "ProjectRecruitRoles" (
"projectId" INTEGER NOT NULL,
"roleId" INTEGER NOT NULL,
"level" INTEGER NOT NULL,
CONSTRAINT "ProjectRecruitRoles_pkey" PRIMARY KEY ("projectId","roleId")
);
-- AddForeignKey
ALTER TABLE "ProjectRecruitRoles" ADD CONSTRAINT "ProjectRecruitRoles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "WorkRole"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectRecruitRoles" ADD CONSTRAINT "ProjectRecruitRoles_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,102 +0,0 @@
-- CreateTable
CREATE TABLE "Tournament" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"thumbnail_image" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"location" TEXT NOT NULL,
"website" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Tournament_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TournamentPrize" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"amount" TEXT NOT NULL,
"image" TEXT NOT NULL,
"tournament_id" INTEGER NOT NULL,
CONSTRAINT "TournamentPrize_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TournamentJudge" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"company" TEXT NOT NULL,
"twitter" TEXT,
"tournament_id" INTEGER NOT NULL,
CONSTRAINT "TournamentJudge_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TournamentFAQ" (
"id" SERIAL NOT NULL,
"question" TEXT NOT NULL,
"answer" TEXT NOT NULL,
"tournament_id" INTEGER NOT NULL,
CONSTRAINT "TournamentFAQ_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TournamentEvent" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"image" TEXT NOT NULL,
"description" TEXT NOT NULL,
"date" DATE NOT NULL,
"location" TEXT NOT NULL,
"website" TEXT NOT NULL,
"type" INTEGER NOT NULL,
"tournament_id" INTEGER NOT NULL,
CONSTRAINT "TournamentEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TournamentParticipant" (
"tournament_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
CONSTRAINT "TournamentParticipant_pkey" PRIMARY KEY ("tournament_id","user_id")
);
-- CreateTable
CREATE TABLE "TournamentProject" (
"tournament_id" INTEGER NOT NULL,
"project_id" INTEGER NOT NULL,
CONSTRAINT "TournamentProject_pkey" PRIMARY KEY ("tournament_id","project_id")
);
-- AddForeignKey
ALTER TABLE "TournamentPrize" ADD CONSTRAINT "TournamentPrize_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentJudge" ADD CONSTRAINT "TournamentJudge_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentFAQ" ADD CONSTRAINT "TournamentFAQ_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentEvent" ADD CONSTRAINT "TournamentEvent_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentParticipant" ADD CONSTRAINT "TournamentParticipant_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentParticipant" ADD CONSTRAINT "TournamentParticipant_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentProject" ADD CONSTRAINT "TournamentProject_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentProject" ADD CONSTRAINT "TournamentProject_tournament_id_fkey" FOREIGN KEY ("tournament_id") REFERENCES "Tournament"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `date` on the `TournamentEvent` table. All the data in the column will be lost.
- Added the required column `ends_at` to the `TournamentEvent` table without a default value. This is not possible if the table is not empty.
- Added the required column `starts_at` to the `TournamentEvent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "TournamentEvent" DROP COLUMN "date",
ADD COLUMN "ends_at" DATE NOT NULL,
ADD COLUMN "starts_at" DATE NOT NULL;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `avatar` to the `TournamentJudge` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "TournamentJudge" ADD COLUMN "avatar" TEXT NOT NULL;

View File

@@ -1,10 +0,0 @@
/*
Warnings:
- Added the required column `email` to the `TournamentParticipant` table without a default value. This is not possible if the table is not empty.
- Added the required column `hacking_status` to the `TournamentParticipant` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "TournamentParticipant" ADD COLUMN "email" TEXT NOT NULL,
ADD COLUMN "hacking_status" INTEGER NOT NULL;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "TournamentParticipant" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "UserKey" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "discord" TEXT;

View File

@@ -1,10 +0,0 @@
-- AlterTable
ALTER TABLE "Donation" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMP(3);
-- AlterTable
ALTER TABLE "Tournament" ALTER COLUMN "start_date" SET DATA TYPE TIMESTAMP(3),
ALTER COLUMN "end_date" SET DATA TYPE TIMESTAMP(3);
-- AlterTable
ALTER TABLE "TournamentEvent" ALTER COLUMN "ends_at" SET DATA TYPE TIMESTAMP(3),
ALTER COLUMN "starts_at" SET DATA TYPE TIMESTAMP(3);

View File

@@ -1,74 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[image_id]` on the table `Award` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Category` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Hackathon` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[thumbnail_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Story` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[avatar_id]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Award" ADD COLUMN "image_id" INTEGER;
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Hackathon" ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "cover_image_id" INTEGER,
ADD COLUMN "screenshots_ids" INTEGER[],
ADD COLUMN "thumbnail_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "body_image_ids" INTEGER[],
ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "avatar_id" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "Award_image_id_key" ON "Award"("image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Category_cover_image_id_key" ON "Category"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Hackathon_cover_image_id_key" ON "Hackathon"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Project_thumbnail_image_id_key" ON "Project"("thumbnail_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Project_cover_image_id_key" ON "Project"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Story_cover_image_id_key" ON "Story"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "User_avatar_id_key" ON "User"("avatar_id");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_thumbnail_image_id_fkey" FOREIGN KEY ("thumbnail_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Award" ADD CONSTRAINT "Award_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Hackathon" ADD CONSTRAINT "Hackathon_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,52 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[thumbnail_image_id]` on the table `Tournament` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Tournament` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[image_id]` on the table `TournamentEvent` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[avatar_id]` on the table `TournamentJudge` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[image_id]` on the table `TournamentPrize` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Tournament" ADD COLUMN "cover_image_id" INTEGER,
ADD COLUMN "thumbnail_image_id" INTEGER;
-- AlterTable
ALTER TABLE "TournamentEvent" ADD COLUMN "image_id" INTEGER;
-- AlterTable
ALTER TABLE "TournamentJudge" ADD COLUMN "avatar_id" INTEGER;
-- AlterTable
ALTER TABLE "TournamentPrize" ADD COLUMN "image_id" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "Tournament_thumbnail_image_id_key" ON "Tournament"("thumbnail_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Tournament_cover_image_id_key" ON "Tournament"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "TournamentEvent_image_id_key" ON "TournamentEvent"("image_id");
-- CreateIndex
CREATE UNIQUE INDEX "TournamentJudge_avatar_id_key" ON "TournamentJudge"("avatar_id");
-- CreateIndex
CREATE UNIQUE INDEX "TournamentPrize_image_id_key" ON "TournamentPrize"("image_id");
-- AddForeignKey
ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_thumbnail_image_id_fkey" FOREIGN KEY ("thumbnail_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentPrize" ADD CONSTRAINT "TournamentPrize_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentJudge" ADD CONSTRAINT "TournamentJudge_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentEvent" ADD CONSTRAINT "TournamentEvent_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,38 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "discord" TEXT NOT NULL DEFAULT E'',
ADD COLUMN "github" TEXT NOT NULL DEFAULT E'',
ADD COLUMN "hashtag" TEXT NOT NULL DEFAULT E'',
ADD COLUMN "launch_status" TEXT NOT NULL DEFAULT E'',
ADD COLUMN "tagline" TEXT NOT NULL DEFAULT E'',
ADD COLUMN "twitter" TEXT NOT NULL DEFAULT E'';
-- CreateTable
CREATE TABLE "Capability" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"icon" TEXT,
"is_official" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Capability_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_CapabilityToProject" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Capability_title_key" ON "Capability"("title");
-- CreateIndex
CREATE UNIQUE INDEX "_CapabilityToProject_AB_unique" ON "_CapabilityToProject"("A", "B");
-- CreateIndex
CREATE INDEX "_CapabilityToProject_B_index" ON "_CapabilityToProject"("B");
-- AddForeignKey
ALTER TABLE "_CapabilityToProject" ADD FOREIGN KEY ("A") REFERENCES "Capability"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CapabilityToProject" ADD FOREIGN KEY ("B") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,18 +0,0 @@
/*
Warnings:
- You are about to drop the column `is_official` on the `Capability` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Capability" DROP COLUMN "is_official";
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "slack" TEXT,
ADD COLUMN "telegram" TEXT,
ALTER COLUMN "discord" DROP NOT NULL,
ALTER COLUMN "discord" DROP DEFAULT,
ALTER COLUMN "github" DROP NOT NULL,
ALTER COLUMN "github" DROP DEFAULT,
ALTER COLUMN "twitter" DROP NOT NULL,
ALTER COLUMN "twitter" DROP DEFAULT;

View File

@@ -1,14 +0,0 @@
-- CreateTable
CREATE TABLE "ProjectMember" (
"projectId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"level" TEXT NOT NULL,
CONSTRAINT "ProjectMember_pkey" PRIMARY KEY ("projectId","userId")
);
-- AddForeignKey
ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `level` on the `ProjectMember` table. All the data in the column will be lost.
- Added the required column `role` to the `ProjectMember` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "ProjectMember" DROP COLUMN "level",
ADD COLUMN "role" TEXT NOT NULL;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ALTER COLUMN "launch_status" SET DEFAULT E'Launched';

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "contact_email" TEXT;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "project_id" INTEGER;
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

Some files were not shown because too many files have changed in this diff Show More