├── .env.template ├── .eslintrc.js ├── .github └── workflows │ ├── rpgf3-deploy.yml │ ├── rpgf4-production-deploy.yml │ ├── rpgf4-staging-deploy.yml │ ├── rpgf5-production-deploy.yml │ └── rpgf5-staging-deploy.yml ├── .gitignore ├── .prettierrc ├── Caddyfile ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── points_snapshot.csv ├── docker-compose-prod.yml ├── docker-compose-staging.yml ├── docker-compose.yml ├── funding.json ├── nest-cli.json ├── package.json ├── prisma ├── migrations │ ├── 20230727105916_init │ │ └── migration.sql │ ├── 20230731200449_mg │ │ └── migration.sql │ ├── 20230804123522_ │ │ └── migration.sql │ ├── 20230804125546_ │ │ └── migration.sql │ ├── 20230807084450_ │ │ └── migration.sql │ ├── 20230811133522_ │ │ └── migration.sql │ ├── 20230820203153_ │ │ └── migration.sql │ ├── 20230822144543_ │ │ └── migration.sql │ ├── 20230926182935_ │ │ └── migration.sql │ ├── 20230926183910_ │ │ └── migration.sql │ ├── 20231001121107_ │ │ └── migration.sql │ ├── 20231001175511_ │ │ └── migration.sql │ ├── 20231006145207_ │ │ └── migration.sql │ ├── 20231010192405_ │ │ └── migration.sql │ ├── 20231027135217_ │ │ └── migration.sql │ ├── 20231030110203_ │ │ └── migration.sql │ ├── 20231102104305_ │ │ └── migration.sql │ ├── 20231103121224_ │ │ └── migration.sql │ ├── 20231105104250_ │ │ └── migration.sql │ ├── 20240423105520_ │ │ └── migration.sql │ ├── 20240423124828_ │ │ └── migration.sql │ ├── 20240423125156_ │ │ └── migration.sql │ ├── 20240430104145_ │ │ └── migration.sql │ ├── 20240430160526_ │ │ └── migration.sql │ ├── 20240518191441_ │ │ └── migration.sql │ ├── 20240603141921_ │ │ └── migration.sql │ ├── 20240610150253_ │ │ └── migration.sql │ ├── 20240617170023_ │ │ └── migration.sql │ ├── 20240618122713_ │ │ └── migration.sql │ ├── 20240627093345_ │ │ └── migration.sql │ ├── 20240628084417_ │ │ └── migration.sql │ ├── 20240630094629_ │ │ └── migration.sql │ ├── 20240815150505_ │ │ └── migration.sql │ ├── 20240915094957_ │ │ └── migration.sql │ ├── 20240915173005_ │ │ └── migration.sql │ ├── 20240915191834_ │ │ └── migration.sql │ ├── 20240920095026_ │ │ └── migration.sql │ ├── 20240928110832_ │ │ └── migration.sql │ ├── 20241003194312_ │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seeds │ ├── pwcat.xlsx │ └── s4data.ts ├── projects.csv ├── src ├── analytics │ ├── analytics.controller.ts │ ├── analytics.module.ts │ └── analytics.service.ts ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.controller.ts │ ├── auth.guard.ts │ ├── auth.module.ts │ ├── auth.service.ts │ └── dto │ │ ├── login.dto.ts │ │ └── otp.dto.ts ├── collection │ ├── colleciton.module.ts │ ├── collection.controller.ts │ └── collection.service.ts ├── flow │ ├── dto │ │ ├── bodies.ts │ │ ├── editedRanking.ts │ │ ├── pairsResult.ts │ │ ├── share.dto.ts │ │ ├── voteCollections.dto.ts │ │ └── voteProjects.dto.ts │ ├── flow.controller.ts │ ├── flow.module.ts │ ├── flow.service.ts │ └── types │ │ └── index.ts ├── main.ts ├── mock │ ├── mock.controller.ts │ └── mock.module.ts ├── prisma.service.ts ├── project-reading │ └── index.ts ├── project │ ├── project.controller.ts │ ├── project.module.ts │ └── project.service.ts ├── rpgf5-data-import │ ├── all-projects-928.d.ts │ ├── all-projects-928.ts │ ├── all-projects-930.d.ts │ ├── all-projects-930.ts │ ├── all-projects-Sep22.d.ts │ ├── all-projects-Sep22.ts │ ├── badgeholders.ts │ ├── ballot.json │ ├── gsheet.ts │ ├── icats-922.ts │ ├── icats-insert.ts │ ├── index copy.ts │ ├── index.ts │ ├── submit.ts │ ├── temp-total.ts │ ├── temp.ts │ ├── types.ts │ └── update-metadata.ts ├── user │ ├── dto │ │ └── ConnectFlowDTOs.ts │ ├── users.controller.ts │ ├── users.module.ts │ └── users.service.ts └── utils │ ├── badges │ ├── index.ts │ ├── production-snapshot.ts │ ├── readBadges.ts │ ├── snapshot.ts │ ├── snapshotQa.ts │ ├── test-snapshots.ts │ └── type.ts │ ├── index.ts │ ├── mathematical-logic │ └── index.ts │ └── types │ └── AuthedReq.type.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env.template: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://username:password@host/postgres?connect_timeout=30&pool_timeout=30&socket_timeout=30" 2 | NODE_ENV="staging" 3 | PORT=7070 4 | PW_BACKEND_URL="https://pw-backend.domain.com" 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | "@typescript-eslint/no-floating-promises": ["error"] 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/rpgf3-deploy.yml: -------------------------------------------------------------------------------- 1 | name: rpgf3-deploy-pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - rpgf3 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: SSH and Redeploy Production 13 | uses: appleboy/ssh-action@v1.0.0 14 | with: 15 | host: ${{ secrets.PROD_HOST }} 16 | username: ${{ secrets.PROD_USERNAME }} 17 | key: ${{ secrets.PROD_PRIVATE_KEY }} 18 | port: ${{ secrets.SSH_PORT }} 19 | script: | 20 | cd pw-backend 21 | docker-compose down -v 22 | docker image prune -a --force 23 | git checkout rpgf3 24 | git pull origin rpgf3 25 | docker-compose up -d --build 26 | 27 | - name: SSH and Redeploy Staging 28 | uses: appleboy/ssh-action@v1.0.0 29 | with: 30 | host: ${{ secrets.STAGING_HOST }} 31 | username: ${{ secrets.STAGING_USERNAME }} 32 | key: ${{ secrets.STAGING_PRIVATE_KEY }} 33 | port: ${{ secrets.SSH_PORT }} 34 | script: | 35 | cd pw-backend 36 | docker-compose down -v 37 | docker image prune -a --force 38 | git checkout rpgf3 39 | git pull origin rpgf3 40 | docker-compose up -d --build -------------------------------------------------------------------------------- /.github/workflows/rpgf4-production-deploy.yml: -------------------------------------------------------------------------------- 1 | name: rf4-deploy-pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - rf4 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: SSH and Redeploy rf4 13 | uses: appleboy/ssh-action@v1.0.0 14 | with: 15 | host: ${{ secrets.PROD_HOST }} 16 | username: ${{ secrets.PROD_USERNAME }} 17 | key: ${{ secrets.PROD_PRIVATE_KEY }} 18 | port: ${{ secrets.SSH_PORT }} 19 | script: | 20 | cd rpgf4-pw-backend 21 | docker-compose down -v 22 | docker image prune -a --force 23 | git checkout rf4 24 | git reset --hard origin/rf4 25 | git pull origin rf4 26 | docker-compose build --no-cache 27 | docker-compose up -d -------------------------------------------------------------------------------- /.github/workflows/rpgf4-staging-deploy.yml: -------------------------------------------------------------------------------- 1 | name: rf4-staging-deploy-pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - rf4-staging 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: SSH and Redeploy rf4-staging 13 | uses: appleboy/ssh-action@v1.0.0 14 | with: 15 | host: ${{ secrets.STAGING_HOST }} 16 | username: ${{ secrets.STAGING_USERNAME }} 17 | key: ${{ secrets.STAGING_PRIVATE_KEY }} 18 | port: ${{ secrets.SSH_PORT }} 19 | script: | 20 | cd rpgf4-pw-backend 21 | docker-compose down -v 22 | docker system prune -af 23 | git checkout rf4-staging 24 | git reset --hard origin/rf4-staging 25 | git pull origin rf4-staging 26 | docker-compose build --no-cache 27 | docker-compose up -d 28 | -------------------------------------------------------------------------------- /.github/workflows/rpgf5-production-deploy.yml: -------------------------------------------------------------------------------- 1 | name: rpgf5-prod-deploy-pipeline 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v3 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v3 19 | - name: Login to GitHub Container Registry 20 | uses: docker/login-action@v3 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Build and push 26 | uses: docker/build-push-action@v6 27 | with: 28 | context: . 29 | push: true 30 | tags: ghcr.io/generalmagicio/rpgf5-be:main 31 | 32 | deploy: 33 | runs-on: ubuntu-latest 34 | needs: publish 35 | steps: 36 | - name: SSH and Redeploy Production 37 | uses: appleboy/ssh-action@v1.0.0 38 | with: 39 | host: ${{ secrets.RPGF5_PROD_HOST }} 40 | username: ${{ secrets.RPGF5_PROD_USERNAME }} 41 | key: ${{ secrets.RPGF5_PROD_PRIVATE_KEY }} 42 | port: ${{ secrets.SSH_PORT }} 43 | script: | 44 | cd pw-backend 45 | git reset --hard HEAD~1 46 | git checkout master 47 | git pull origin master 48 | docker image prune -a --force 49 | docker compose -f docker-compose-prod.yml pull 50 | 51 | ## Update each backend service one by one 52 | docker compose -f docker-compose-prod.yml up -d --no-deps --scale pw-backend1=0 --scale pw-backend2=1 53 | docker compose -f docker-compose-prod.yml up -d 54 | # Check the health of pw-backend1 55 | if [ "$(docker inspect --format='{{json .State.Status}}' pw-backend1)" != "\"running\"" ]; then 56 | echo "pw-backend1 is not running, stopping deployment" 57 | exit 1 58 | fi 59 | 60 | docker compose -f docker-compose-prod.yml up -d --no-deps --scale pw-backend1=1 --scale pw-backend2=0 61 | docker compose -f docker-compose-prod.yml up -d 62 | # Check the health of pw-backend2 63 | if [ "$(docker inspect --format='{{json .State.Status}}' pw-backend2)" != "\"running\"" ]; then 64 | echo "pw-backend2 is not running, stopping deployment" 65 | exit 1 66 | fi -------------------------------------------------------------------------------- /.github/workflows/rpgf5-staging-deploy.yml: -------------------------------------------------------------------------------- 1 | name: rpgf5-staging-deploy-pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v3 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | - name: Login to GitHub Container Registry 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Build and push 25 | uses: docker/build-push-action@v6 26 | with: 27 | context: . 28 | push: true 29 | tags: ghcr.io/generalmagicio/rpgf5-be:staging 30 | 31 | deploy: 32 | runs-on: ubuntu-latest 33 | needs: publish 34 | steps: 35 | - name: SSH and Redeploy Production 36 | uses: appleboy/ssh-action@v1.0.0 37 | with: 38 | host: ${{ secrets.RPGF5_STAGING_HOST }} 39 | username: ${{ secrets.RPGF5_STAGING_USERNAME }} 40 | key: ${{ secrets.RPGF5_STAGING_PRIVATE_KEY }} 41 | port: ${{ secrets.SSH_PORT }} 42 | script: | 43 | cd pw-backend 44 | git reset --hard HEAD~1 45 | git checkout staging 46 | git pull origin staging 47 | docker image prune -a --force 48 | docker compose -f docker-compose-staging.yml pull 49 | 50 | ## Update each backend service one by one 51 | docker compose -f docker-compose-staging.yml up -d --no-deps --scale pw-backend1=0 --scale pw-backend2=1 52 | docker compose -f docker-compose-staging.yml up -d 53 | # Check the health of pw-backend1 54 | if [ "$(docker inspect --format='{{json .State.Status}}' pw-backend1)" != "\"running\"" ]; then 55 | echo "pw-backend1 is not running, stopping deployment" 56 | exit 1 57 | fi 58 | 59 | docker compose -f docker-compose-staging.yml up -d --no-deps --scale pw-backend1=1 --scale pw-backend2=0 60 | docker compose -f docker-compose-staging.yml up -d 61 | # Check the health of pw-backend2 62 | if [ "$(docker inspect --format='{{json .State.Status}}' pw-backend2)" != "\"running\"" ]; then 63 | echo "pw-backend2 is not running, stopping deployment" 64 | exit 1 65 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /certs 5 | /src/project-reading 6 | # /src/rpgf5-data-import 7 | /src/ai-summary 8 | 9 | # Envs 10 | 11 | .env 12 | .env.* 13 | .env* 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | pnpm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | lerna-debug.log* 23 | 24 | # outputs 25 | /src/results 26 | 27 | # OS 28 | .DS_Store 29 | 30 | # Tests 31 | /coverage 32 | /.nyc_output 33 | 34 | # IDEs and editors 35 | /.idea 36 | .project 37 | .classpath 38 | .c9/ 39 | *.launch 40 | .settings/ 41 | *.sublime-workspace 42 | 43 | # IDE - VSCode 44 | .vscode/* 45 | !.vscode/settings.json 46 | !.vscode/tasks.json 47 | !.vscode/launch.json 48 | !.vscode/extensions.json 49 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | {$BACKEND_URL} { 2 | route { 3 | @allowed { 4 | path /* 5 | remote_ip {$IP_WHITELIST} 6 | } 7 | reverse_proxy @allowed { 8 | to pw-backend1:7070 pw-backend2:7070 9 | lb_policy round_robin 10 | health_uri / 11 | health_interval 5s 12 | health_timeout 2s 13 | health_status 200 14 | } 15 | respond 403 16 | } 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | COPY tsconfig*.json ./ 6 | RUN npm install -g @nestjs/cli 7 | RUN npm install 8 | COPY . . 9 | RUN npm run build 10 | EXPOSE 7070 11 | ENTRYPOINT ["sh", "-c", "npx prisma migrate deploy && npm run start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /assets/points_snapshot.csv: -------------------------------------------------------------------------------- 1 | 2 | User,holderPoints,delegatePoints,recipientsPoints,badgeholderPoints 3 | olimpio.eth,50,1,1,1 4 | GFX Labs,0,5,1,0 5 | 0x1689...D374,0,5,0,0 6 | Lefteris Karapetsas …,0,0,0,1 7 | 0x406b...c159,0,10,0,1 8 | moenick.eth,25,7,0,0 9 | jackanorak.eth,50,10,0,0 10 | notscottmoore,25,10,1,1 11 | Michael Vander Meide…,50,0,0,0 12 | she256.eth,0,3,0,0 13 | Penn Blockchain,15,0,1,1 14 | she256.eth,0,5,0,0 15 | 0x46ab...b4d1,0,1,1,0 16 | 0x3D2d...937C,0,5,0,0 17 | katiegarcia.eth,0,3,0,0 18 | joxes.eth,0,0,0,1 19 | hi_Reverie,0,0,0,1 20 | 404 DAO,0,0,0,1 21 | 0x80FC...E482,0,7,0,0 22 | 0x9950...3Cf4,0,2,0,1 23 | 0x8Ccd...f98d,0,5,0,0 24 | 0x11cf...d752,0,7,0,0 25 | PGov,0,0,1,1 26 | Tané,15,0,0,0 27 | 0x2B78...88f8,50,0,0,0 28 | 0x2dEB...C399,50,0,0,0 29 | 0x3951...A552,5,0,0,0 30 | Wintermute Governanc…,0,10,0,0 31 | jacob.willemsma.eth,0,3,0,0 32 | 0xB1EA...8251,0,7,0,0 33 | mihal.eth,0,2,0,0 34 | Griff Green 🙏🌱🎗💜,5,2,1,1 35 | oilysirs.eth,25,3,1,1 36 | mjs.eth,25,7,1,1 37 | theethernaut.eth,50,7,1,1 38 | Flipside Crypto,50,5,1,1 39 | 0x8F54...1CC3,0,7,0,0 40 | Rick Dudley (afdudle…,0,3,0,0 41 | yoav.eth,5,1,0,0 42 | 0xE1f8...0F53,0,3,0,0 43 | CalBlockchain,50,0,1,1 44 | 0xD0c4...B3D9,25,0,1,1 45 | discusfish.eth,50,0,1,1 46 | 0x9CBE...0a40,25,0,1,1 47 | sugma.eth,50,0,1,1 48 | Layer3,50,0,1,1 49 | mattlosquadro.eth,25,0,1,1 50 | 0xCc87...4c62,50,0,1,1 51 | MinimalGravitas,0,3,0,0 52 | pseudotheos.eth,25,7,0,0 53 | @ewokafloka,50,5,0,0 54 | 0x9b7D...E532,50,7,0,0 55 | 0x1383...Ae2A,0,10,0,0 56 | 0x2C6c...2B63,0,5,0,1 57 | solarcurve.eth,0,5,0,1 58 | dhannte.eth,0,2,0,1 59 | 0xc94a...c0Cd,0,5,0,1 60 | butterbum.eth,0,5,0,0 61 | 0xb074...8012,15,2,1,0 62 | @0xDonPepe,3,7,1,0 63 | forrestn.eth,50,7,1,0 64 | 0xdab0...5f5A,0,3,0,0 65 | nathanvdh.eth,100,5,0,0 66 | 0xeeF4...2018,50,7,0,0 67 | tongnk.eth,100,2,0,0 68 | Exosphere,50,3,0,0 69 | ercwl,50,1,0,0 70 | 0x0f97...4496,50,10,0,0 71 | mastermojo.eth,15,5,0,0 72 | advantageblockchain.…,25,3,0,0 73 | 0x0f94...9230,25,3,0,0 74 | esmeralda.eth,50,7,0,0 75 | itublockchain.eth,50,10,0,0 76 | hailvitalik.eth,25,10,0,0 77 | voiceof.eth,100,10,0,0 78 | web3magnetic,15,2,0,0 79 | blockchainathopkins.…,50,10,0,0 80 | "For StableLab, pleas…",25,2,0,0 81 | scopelift.eth,0,5,0,0 82 | 0xA3Eb...836a,0,3,0,0 83 | 0x03dD...92fA,25,3,1,0 84 | Ryan H.,0,2,0,0 85 | mikegriff.eth,50,0,0,0 86 | 0xc420...073c,25,0,0,0 87 | LaVeP,15,0,0,0 88 | 0x5a7F...0BA2,50,0,0,0 89 | 0x73Ec...b07d,5,0,0,0 90 | 0x316131DC685A63B1dbC8E0Fc6B893ec51CF159DA,10,10,1,0 91 | 0x393053056EB678EA95CBc67CB7E1198184984707,5,15,1,1 92 | 0xc0f2A154abA3f12D71AF25e87ca4f225B9C52203,4,6,0,1 93 | 0xcd192b61a8Dd586A97592555c1f5709e032F2505,15,2,1,1 94 | 0xA1179f64638adb613DDAAc32D918EB6BEB824104,2,0,0,1 95 | 0x6eb78c56F639b3d161456e9f893c8e8aD9d754F0,0,7,1,0 96 | 0xD5db3F8B0a236176587460dC85F0fC5705D78477,5,5,0,1 97 | 0xe1e5dcbbc95aabe80e2f9c65c7a2cef85daf61c4,0,5,0,0 98 | 0x33878e070db7f70D2953Fe0278Cd32aDf8104572,20,5,0,0 -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pw-backend1: 5 | image: ghcr.io/generalmagicio/rpgf5-be:main 6 | container_name: pw-backend1 7 | restart: always 8 | ports: 9 | - 7070 10 | env_file: 11 | - .env 12 | networks: 13 | - pw-backend 14 | 15 | pw-backend2: 16 | image: ghcr.io/generalmagicio/rpgf5-be:main 17 | container_name: pw-backend2 18 | restart: always 19 | ports: 20 | - 7070 21 | env_file: 22 | - .env 23 | networks: 24 | - pw-backend 25 | 26 | caddy: 27 | image: caddy:2-alpine 28 | container_name: caddy 29 | restart: unless-stopped 30 | networks: 31 | - pw-backend 32 | ports: 33 | - 80:80 34 | - 443:443 35 | env_file: 36 | - .env 37 | environment: 38 | - BACKEND_URL=${BACKEND_URL:-} 39 | - IP_WHITELIST=${IP_WHITELIST:-0.0.0.0/0} 40 | volumes: 41 | - caddy_data:/data 42 | - caddy_config:/config 43 | - ./Caddyfile:/etc/caddy/Caddyfile 44 | 45 | volumes: 46 | caddy_config: 47 | caddy_data: 48 | 49 | networks: 50 | pw-backend: -------------------------------------------------------------------------------- /docker-compose-staging.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pw-backend1: 5 | image: ghcr.io/generalmagicio/rpgf5-be:staging 6 | container_name: pw-backend1 7 | restart: always 8 | ports: 9 | - 7070 10 | env_file: 11 | - .env 12 | networks: 13 | - pw-backend 14 | 15 | pw-backend2: 16 | image: ghcr.io/generalmagicio/rpgf5-be:staging 17 | container_name: pw-backend2 18 | restart: always 19 | ports: 20 | - 7070 21 | env_file: 22 | - .env 23 | networks: 24 | - pw-backend 25 | 26 | caddy: 27 | image: caddy:2-alpine 28 | container_name: caddy 29 | restart: unless-stopped 30 | networks: 31 | - pw-backend 32 | ports: 33 | - 80:80 34 | - 443:443 35 | env_file: 36 | - .env 37 | environment: 38 | - BACKEND_URL=${BACKEND_URL:-} 39 | - IP_WHITELIST=${IP_WHITELIST:-0.0.0.0/0} 40 | volumes: 41 | - caddy_data:/data 42 | - caddy_config:/config 43 | - ./Caddyfile:/etc/caddy/Caddyfile 44 | 45 | volumes: 46 | caddy_config: 47 | caddy_data: 48 | 49 | networks: 50 | pw-backend: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pw-backend: 5 | build: 6 | context: . 7 | container_name: rpgf4-pw-backend 8 | restart: always 9 | ports: 10 | - 7071:7070 11 | networks: 12 | - pw-backend 13 | volumes: 14 | - ./data:/usr/src/app/data 15 | 16 | networks: 17 | pw-backend: -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x98877a3c5f3d5eee496386ae93a23b17f0f51b70b3041b3c8226f98fbeca09ec" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "assets": ["assets/**/*", "**/*.csv"], 8 | "watchAssets": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pairwise-back", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "npx prisma generate && rm -rf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "node dist/src/main", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "seed": "cd prisma && cd seeds && ts-node ./seed.ts", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^10.0.0", 26 | "@nestjs/config": "^3.0.0", 27 | "@nestjs/core": "^10.0.0", 28 | "@nestjs/platform-express": "^10.0.0", 29 | "@nestjs/swagger": "^7.1.6", 30 | "@prisma/client": "^5.0.0", 31 | "@types/bcrypt": "^5.0.0", 32 | "@types/cookie-parser": "^1.4.3", 33 | "@types/cors": "^2.8.13", 34 | "axios": "^1.6.0", 35 | "bcrypt": "^5.1.0", 36 | "class-transformer": "^0.5.1", 37 | "class-validator": "^0.14.0", 38 | "cookie-parser": "^1.4.6", 39 | "cors": "^2.8.5", 40 | "ethers": "^6.12.2", 41 | "form-data": "^4.0.0", 42 | "json2csv": "^6.0.0-alpha.2", 43 | "mathjs": "^11.9.1", 44 | "ml-matrix": "^6.10.5", 45 | "prisma": "^5.0.0", 46 | "reflect-metadata": "^0.1.13", 47 | "rxjs": "^7.8.1", 48 | "siwe": "^2.3.2", 49 | "viem": "^2.21.5" 50 | }, 51 | "devDependencies": { 52 | "@nestjs/cli": "^10.0.0", 53 | "@nestjs/schematics": "^10.0.0", 54 | "@nestjs/testing": "^10.0.0", 55 | "@types/express": "^4.17.17", 56 | "@types/jest": "^29.5.2", 57 | "@types/json2csv": "^5.0.7", 58 | "@types/node": "^20.3.1", 59 | "@types/supertest": "^2.0.12", 60 | "@typescript-eslint/eslint-plugin": "^5.59.11", 61 | "@typescript-eslint/parser": "^5.59.11", 62 | "eslint": "^8.42.0", 63 | "eslint-config-prettier": "^8.8.0", 64 | "eslint-plugin-prettier": "^4.2.1", 65 | "jest": "^29.5.0", 66 | "mkcert": "^1.5.1", 67 | "prettier": "^2.8.8", 68 | "source-map-support": "^0.5.21", 69 | "supertest": "^6.3.3", 70 | "ts-jest": "^29.1.0", 71 | "ts-loader": "^9.4.3", 72 | "ts-node": "^10.9.1", 73 | "tsconfig-paths": "^4.2.0", 74 | "typescript": "^5.1.3", 75 | "xlsx": "^0.18.5" 76 | }, 77 | "jest": { 78 | "moduleFileExtensions": [ 79 | "js", 80 | "json", 81 | "ts" 82 | ], 83 | "rootDir": "src", 84 | "testRegex": ".*\\.spec\\.ts$", 85 | "transform": { 86 | "^.+\\.(t|j)s$": "ts-jest" 87 | }, 88 | "collectCoverageFrom": [ 89 | "**/*.(t|j)s" 90 | ], 91 | "coverageDirectory": "../coverage", 92 | "testEnvironment": "node" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /prisma/migrations/20230727105916_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "PollStatus" AS ENUM ('ACTIVE', 'CLOSED'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "User" ( 6 | "id" SERIAL NOT NULL, 7 | "address" TEXT NOT NULL, 8 | "isBadgeHolder" INTEGER NOT NULL, 9 | "created_at" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "Space" ( 16 | "id" SERIAL NOT NULL, 17 | "title" TEXT NOT NULL, 18 | "description" TEXT NOT NULL, 19 | "created_at" TIMESTAMP(3) NOT NULL, 20 | 21 | CONSTRAINT "Space_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "Poll" ( 26 | "id" SERIAL NOT NULL, 27 | "title" TEXT NOT NULL, 28 | "space_id" INTEGER NOT NULL, 29 | "status" "PollStatus" NOT NULL DEFAULT 'ACTIVE', 30 | "ends_at" TIMESTAMP(3) NOT NULL, 31 | "created_at" TIMESTAMP(3) NOT NULL, 32 | 33 | CONSTRAINT "Poll_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- CreateTable 37 | CREATE TABLE "Collection" ( 38 | "id" SERIAL NOT NULL, 39 | "poll_id" INTEGER NOT NULL, 40 | "parent_collection_id" INTEGER, 41 | "created_at" TIMESTAMP(3) NOT NULL, 42 | 43 | CONSTRAINT "Collection_pkey" PRIMARY KEY ("id") 44 | ); 45 | 46 | -- CreateTable 47 | CREATE TABLE "Result" ( 48 | "id" SERIAL NOT NULL, 49 | "user_id" INTEGER NOT NULL, 50 | "project_id" INTEGER NOT NULL, 51 | "value" DOUBLE PRECISION NOT NULL, 52 | 53 | CONSTRAINT "Result_pkey" PRIMARY KEY ("id") 54 | ); 55 | 56 | -- CreateTable 57 | CREATE TABLE "Project" ( 58 | "id" SERIAL NOT NULL, 59 | "url" TEXT NOT NULL, 60 | "description" TEXT NOT NULL, 61 | "collection_id" INTEGER NOT NULL, 62 | "created_at" TIMESTAMP(3) NOT NULL, 63 | 64 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id") 65 | ); 66 | 67 | -- CreateTable 68 | CREATE TABLE "Vote" ( 69 | "id" SERIAL NOT NULL, 70 | "user_id" INTEGER NOT NULL, 71 | "collection1_id" INTEGER NOT NULL, 72 | "collection2_id" INTEGER NOT NULL, 73 | "picked_id" INTEGER NOT NULL, 74 | "created_at" TIMESTAMP(3) NOT NULL, 75 | 76 | CONSTRAINT "Vote_pkey" PRIMARY KEY ("id") 77 | ); 78 | 79 | -- AddForeignKey 80 | ALTER TABLE "Poll" ADD CONSTRAINT "Poll_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "Space"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 81 | 82 | -- AddForeignKey 83 | ALTER TABLE "Collection" ADD CONSTRAINT "Collection_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "Poll"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 84 | 85 | -- AddForeignKey 86 | ALTER TABLE "Collection" ADD CONSTRAINT "Collection_parent_collection_id_fkey" FOREIGN KEY ("parent_collection_id") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; 87 | 88 | -- AddForeignKey 89 | ALTER TABLE "Result" ADD CONSTRAINT "Result_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 90 | 91 | -- AddForeignKey 92 | ALTER TABLE "Result" ADD CONSTRAINT "Result_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 93 | 94 | -- AddForeignKey 95 | ALTER TABLE "Project" ADD CONSTRAINT "Project_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 96 | 97 | -- AddForeignKey 98 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 99 | 100 | -- AddForeignKey 101 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_collection1_id_fkey" FOREIGN KEY ("collection1_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 102 | 103 | -- AddForeignKey 104 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_collection2_id_fkey" FOREIGN KEY ("collection2_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 105 | 106 | -- AddForeignKey 107 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 108 | -------------------------------------------------------------------------------- /prisma/migrations/20230731200449_mg/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Collection" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Poll" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Project" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Space" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 12 | 13 | -- AlterTable 14 | ALTER TABLE "User" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 15 | 16 | -- AlterTable 17 | ALTER TABLE "Vote" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; 18 | 19 | -- CreateTable 20 | CREATE TABLE "Nonce" ( 21 | "id" SERIAL NOT NULL, 22 | "user_id" INTEGER, 23 | "nonce" TEXT NOT NULL, 24 | "expires_at" TEXT NOT NULL, 25 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 | 27 | CONSTRAINT "Nonce_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateIndex 31 | CREATE UNIQUE INDEX "Nonce_user_id_key" ON "Nonce"("user_id"); 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "Nonce" ADD CONSTRAINT "Nonce_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /prisma/migrations/20230804123522_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `name` to the `Collection` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Collection" ADD COLUMN "name" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230804125546_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `name` to the `Project` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Project" ADD COLUMN "name" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230807084450_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Vote` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_collection1_id_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_collection2_id_fkey"; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_picked_id_fkey"; 15 | 16 | -- DropForeignKey 17 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_user_id_fkey"; 18 | 19 | -- DropTable 20 | DROP TABLE "Vote"; 21 | 22 | -- CreateTable 23 | CREATE TABLE "CollectionVote" ( 24 | "id" SERIAL NOT NULL, 25 | "user_id" INTEGER NOT NULL, 26 | "collection1_id" INTEGER NOT NULL, 27 | "collection2_id" INTEGER NOT NULL, 28 | "picked_id" INTEGER, 29 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | 32 | CONSTRAINT "CollectionVote_pkey" PRIMARY KEY ("id") 33 | ); 34 | 35 | -- CreateTable 36 | CREATE TABLE "ProjectVote" ( 37 | "id" SERIAL NOT NULL, 38 | "user_id" INTEGER NOT NULL, 39 | "project1_id" INTEGER NOT NULL, 40 | "project2_id" INTEGER NOT NULL, 41 | "picked_id" INTEGER, 42 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 43 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 44 | 45 | CONSTRAINT "ProjectVote_pkey" PRIMARY KEY ("id") 46 | ); 47 | 48 | -- CreateIndex 49 | CREATE UNIQUE INDEX "CollectionVote_collection1_id_collection2_id_user_id_key" ON "CollectionVote"("collection1_id", "collection2_id", "user_id"); 50 | 51 | -- CreateIndex 52 | CREATE UNIQUE INDEX "ProjectVote_project1_id_project2_id_user_id_key" ON "ProjectVote"("project1_id", "project2_id", "user_id"); 53 | 54 | -- AddForeignKey 55 | ALTER TABLE "CollectionVote" ADD CONSTRAINT "CollectionVote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "CollectionVote" ADD CONSTRAINT "CollectionVote_collection1_id_fkey" FOREIGN KEY ("collection1_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "CollectionVote" ADD CONSTRAINT "CollectionVote_collection2_id_fkey" FOREIGN KEY ("collection2_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | 63 | -- AddForeignKey 64 | ALTER TABLE "CollectionVote" ADD CONSTRAINT "CollectionVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; 65 | 66 | -- AddForeignKey 67 | ALTER TABLE "ProjectVote" ADD CONSTRAINT "ProjectVote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 68 | 69 | -- AddForeignKey 70 | ALTER TABLE "ProjectVote" ADD CONSTRAINT "ProjectVote_project1_id_fkey" FOREIGN KEY ("project1_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "ProjectVote" ADD CONSTRAINT "ProjectVote_project2_id_fkey" FOREIGN KEY ("project2_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 74 | 75 | -- AddForeignKey 76 | ALTER TABLE "ProjectVote" ADD CONSTRAINT "ProjectVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 77 | -------------------------------------------------------------------------------- /prisma/migrations/20230811133522_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Collection" ADD COLUMN "image" TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Project" ADD COLUMN "image" TEXT; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20230820203153_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ExpertiseVote" ( 3 | "id" SERIAL NOT NULL, 4 | "user_id" INTEGER NOT NULL, 5 | "collection1_id" INTEGER NOT NULL, 6 | "collection2_id" INTEGER NOT NULL, 7 | "picked_id" INTEGER, 8 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | 11 | CONSTRAINT "ExpertiseVote_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "ExpertiseVote_collection1_id_collection2_id_user_id_key" ON "ExpertiseVote"("collection1_id", "collection2_id", "user_id"); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection1_id_fkey" FOREIGN KEY ("collection1_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection2_id_fkey" FOREIGN KEY ("collection2_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /prisma/migrations/20230822144543_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserCollectionFinish" ( 3 | "user_id" INTEGER NOT NULL, 4 | "collection_id" INTEGER NOT NULL, 5 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "UserCollectionFinish_pkey" PRIMARY KEY ("user_id","collection_id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "UserCollectionFinish" ADD CONSTRAINT "UserCollectionFinish_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "UserCollectionFinish" ADD CONSTRAINT "UserCollectionFinish_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20230926182935_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "EditedRanking" ( 3 | "id" SERIAL NOT NULL, 4 | "user_id" INTEGER NOT NULL, 5 | "ranking" TEXT NOT NULL, 6 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "EditedRanking_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "EditedRanking_user_id_key" ON "EditedRanking"("user_id"); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "EditedRanking" ADD CONSTRAINT "EditedRanking_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20230926183910_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[user_id,collection_id]` on the table `EditedRanking` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "EditedRanking_user_id_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "EditedRanking" ADD COLUMN "collection_id" INTEGER; 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "EditedRanking_user_id_collection_id_key" ON "EditedRanking"("user_id", "collection_id"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "EditedRanking" ADD CONSTRAINT "EditedRanking_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20231001121107_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "SubProject" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "url" TEXT NOT NULL, 6 | "description" TEXT NOT NULL, 7 | "project_id" INTEGER NOT NULL, 8 | "image" TEXT, 9 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | 11 | CONSTRAINT "SubProject_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "SubProjectVote" ( 16 | "id" SERIAL NOT NULL, 17 | "user_id" INTEGER NOT NULL, 18 | "subProject1Id" INTEGER NOT NULL, 19 | "subProject2Id" INTEGER NOT NULL, 20 | "picked_id" INTEGER, 21 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | 24 | CONSTRAINT "SubProjectVote_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateIndex 28 | CREATE UNIQUE INDEX "SubProjectVote_subProject1Id_subProject2Id_user_id_key" ON "SubProjectVote"("subProject1Id", "subProject2Id", "user_id"); 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "SubProject" ADD CONSTRAINT "SubProject_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "SubProjectVote" ADD CONSTRAINT "SubProjectVote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 35 | 36 | -- AddForeignKey 37 | ALTER TABLE "SubProjectVote" ADD CONSTRAINT "SubProjectVote_subProject1Id_fkey" FOREIGN KEY ("subProject1Id") REFERENCES "SubProject"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 38 | 39 | -- AddForeignKey 40 | ALTER TABLE "SubProjectVote" ADD CONSTRAINT "SubProjectVote_subProject2Id_fkey" FOREIGN KEY ("subProject2Id") REFERENCES "SubProject"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 41 | 42 | -- AddForeignKey 43 | ALTER TABLE "SubProjectVote" ADD CONSTRAINT "SubProjectVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "SubProject"("id") ON DELETE SET NULL ON UPDATE CASCADE; 44 | -------------------------------------------------------------------------------- /prisma/migrations/20231001175511_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserCompositeProjectFinish" ( 3 | "user_id" INTEGER NOT NULL, 4 | "project_id" INTEGER NOT NULL, 5 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "UserCompositeProjectFinish_pkey" PRIMARY KEY ("user_id","project_id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "UserCompositeProjectFinish" ADD CONSTRAINT "UserCompositeProjectFinish_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "UserCompositeProjectFinish" ADD CONSTRAINT "UserCompositeProjectFinish_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20231006145207_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `collection_id` on the `Project` table. All the data in the column will be lost. 5 | - You are about to drop the `Collection` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `CollectionVote` table. If the table is not empty, all the data it contains will be lost. 7 | - You are about to drop the `ProjectVote` table. If the table is not empty, all the data it contains will be lost. 8 | - You are about to drop the `Result` table. If the table is not empty, all the data it contains will be lost. 9 | - You are about to drop the `SubProject` table. If the table is not empty, all the data it contains will be lost. 10 | - You are about to drop the `SubProjectVote` table. If the table is not empty, all the data it contains will be lost. 11 | - You are about to drop the `UserCompositeProjectFinish` table. If the table is not empty, all the data it contains will be lost. 12 | - Added the required column `poll_id` to the `Project` table without a default value. This is not possible if the table is not empty. 13 | - Added the required column `type` to the `Project` table without a default value. This is not possible if the table is not empty. 14 | 15 | */ 16 | -- CreateEnum 17 | CREATE TYPE "ProjectType" AS ENUM ('collection', 'composite_project', 'project'); 18 | 19 | -- DropForeignKey 20 | ALTER TABLE "Collection" DROP CONSTRAINT "Collection_parent_collection_id_fkey"; 21 | 22 | -- DropForeignKey 23 | ALTER TABLE "Collection" DROP CONSTRAINT "Collection_poll_id_fkey"; 24 | 25 | -- DropForeignKey 26 | ALTER TABLE "CollectionVote" DROP CONSTRAINT "CollectionVote_collection1_id_fkey"; 27 | 28 | -- DropForeignKey 29 | ALTER TABLE "CollectionVote" DROP CONSTRAINT "CollectionVote_collection2_id_fkey"; 30 | 31 | -- DropForeignKey 32 | ALTER TABLE "CollectionVote" DROP CONSTRAINT "CollectionVote_picked_id_fkey"; 33 | 34 | -- DropForeignKey 35 | ALTER TABLE "CollectionVote" DROP CONSTRAINT "CollectionVote_user_id_fkey"; 36 | 37 | -- DropForeignKey 38 | ALTER TABLE "EditedRanking" DROP CONSTRAINT "EditedRanking_collection_id_fkey"; 39 | 40 | -- DropForeignKey 41 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection1_id_fkey"; 42 | 43 | -- DropForeignKey 44 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection2_id_fkey"; 45 | 46 | -- DropForeignKey 47 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_picked_id_fkey"; 48 | 49 | -- DropForeignKey 50 | ALTER TABLE "Project" DROP CONSTRAINT "Project_collection_id_fkey"; 51 | 52 | -- DropForeignKey 53 | ALTER TABLE "ProjectVote" DROP CONSTRAINT "ProjectVote_picked_id_fkey"; 54 | 55 | -- DropForeignKey 56 | ALTER TABLE "ProjectVote" DROP CONSTRAINT "ProjectVote_project1_id_fkey"; 57 | 58 | -- DropForeignKey 59 | ALTER TABLE "ProjectVote" DROP CONSTRAINT "ProjectVote_project2_id_fkey"; 60 | 61 | -- DropForeignKey 62 | ALTER TABLE "ProjectVote" DROP CONSTRAINT "ProjectVote_user_id_fkey"; 63 | 64 | -- DropForeignKey 65 | ALTER TABLE "Result" DROP CONSTRAINT "Result_project_id_fkey"; 66 | 67 | -- DropForeignKey 68 | ALTER TABLE "Result" DROP CONSTRAINT "Result_user_id_fkey"; 69 | 70 | -- DropForeignKey 71 | ALTER TABLE "SubProject" DROP CONSTRAINT "SubProject_project_id_fkey"; 72 | 73 | -- DropForeignKey 74 | ALTER TABLE "SubProjectVote" DROP CONSTRAINT "SubProjectVote_picked_id_fkey"; 75 | 76 | -- DropForeignKey 77 | ALTER TABLE "SubProjectVote" DROP CONSTRAINT "SubProjectVote_subProject1Id_fkey"; 78 | 79 | -- DropForeignKey 80 | ALTER TABLE "SubProjectVote" DROP CONSTRAINT "SubProjectVote_subProject2Id_fkey"; 81 | 82 | -- DropForeignKey 83 | ALTER TABLE "SubProjectVote" DROP CONSTRAINT "SubProjectVote_user_id_fkey"; 84 | 85 | -- DropForeignKey 86 | ALTER TABLE "UserCollectionFinish" DROP CONSTRAINT "UserCollectionFinish_collection_id_fkey"; 87 | 88 | -- DropForeignKey 89 | ALTER TABLE "UserCompositeProjectFinish" DROP CONSTRAINT "UserCompositeProjectFinish_project_id_fkey"; 90 | 91 | -- DropForeignKey 92 | ALTER TABLE "UserCompositeProjectFinish" DROP CONSTRAINT "UserCompositeProjectFinish_user_id_fkey"; 93 | 94 | -- AlterTable 95 | ALTER TABLE "Project" DROP COLUMN "collection_id", 96 | ADD COLUMN "parentId" INTEGER, 97 | ADD COLUMN "poll_id" INTEGER NOT NULL, 98 | ADD COLUMN "type" "ProjectType" NOT NULL; 99 | 100 | -- DropTable 101 | DROP TABLE "Collection"; 102 | 103 | -- DropTable 104 | DROP TABLE "CollectionVote"; 105 | 106 | -- DropTable 107 | DROP TABLE "ProjectVote"; 108 | 109 | -- DropTable 110 | DROP TABLE "Result"; 111 | 112 | -- DropTable 113 | DROP TABLE "SubProject"; 114 | 115 | -- DropTable 116 | DROP TABLE "SubProjectVote"; 117 | 118 | -- DropTable 119 | DROP TABLE "UserCompositeProjectFinish"; 120 | 121 | -- CreateTable 122 | CREATE TABLE "Vote" ( 123 | "id" SERIAL NOT NULL, 124 | "user_id" INTEGER NOT NULL, 125 | "project1_id" INTEGER NOT NULL, 126 | "project2_id" INTEGER NOT NULL, 127 | "picked_id" INTEGER, 128 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 129 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 130 | 131 | CONSTRAINT "Vote_pkey" PRIMARY KEY ("id") 132 | ); 133 | 134 | -- CreateIndex 135 | CREATE UNIQUE INDEX "Vote_project1_id_project2_id_user_id_key" ON "Vote"("project1_id", "project2_id", "user_id"); 136 | 137 | -- AddForeignKey 138 | ALTER TABLE "Project" ADD CONSTRAINT "Project_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 139 | 140 | -- AddForeignKey 141 | ALTER TABLE "Project" ADD CONSTRAINT "Project_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "Poll"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 142 | 143 | -- AddForeignKey 144 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 145 | 146 | -- AddForeignKey 147 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project1_id_fkey" FOREIGN KEY ("project1_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 148 | 149 | -- AddForeignKey 150 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project2_id_fkey" FOREIGN KEY ("project2_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 151 | 152 | -- AddForeignKey 153 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 154 | 155 | -- AddForeignKey 156 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection1_id_fkey" FOREIGN KEY ("collection1_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 157 | 158 | -- AddForeignKey 159 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection2_id_fkey" FOREIGN KEY ("collection2_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 160 | 161 | -- AddForeignKey 162 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 163 | 164 | -- AddForeignKey 165 | ALTER TABLE "EditedRanking" ADD CONSTRAINT "EditedRanking_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 166 | 167 | -- AddForeignKey 168 | ALTER TABLE "UserCollectionFinish" ADD CONSTRAINT "UserCollectionFinish_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 169 | -------------------------------------------------------------------------------- /prisma/migrations/20231010192405_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `EditedRanking` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "EditedRanking" DROP CONSTRAINT "EditedRanking_collection_id_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "EditedRanking" DROP CONSTRAINT "EditedRanking_user_id_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "EditedRanking"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "Share" ( 18 | "id" SERIAL NOT NULL, 19 | "user_id" INTEGER NOT NULL, 20 | "project_id" INTEGER NOT NULL, 21 | "share" DOUBLE PRECISION NOT NULL, 22 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 24 | 25 | CONSTRAINT "Share_pkey" PRIMARY KEY ("id") 26 | ); 27 | 28 | -- CreateIndex 29 | CREATE UNIQUE INDEX "Share_user_id_project_id_key" ON "Share"("user_id", "project_id"); 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "Share" ADD CONSTRAINT "Share_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "Share" ADD CONSTRAINT "Share_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /prisma/migrations/20231027135217_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `Share` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `id` on the `Share` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- DropIndex 9 | DROP INDEX "Share_user_id_project_id_key"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Share" DROP CONSTRAINT "Share_pkey", 13 | DROP COLUMN "id", 14 | ADD CONSTRAINT "Share_pkey" PRIMARY KEY ("user_id", "project_id"); 15 | -------------------------------------------------------------------------------- /prisma/migrations/20231030110203_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "RPGF3Id" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20231102104305_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `description` on the `Project` table. All the data in the column will be lost. 5 | - Added the required column `impactDescription` to the `Project` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Project" DROP COLUMN "description", 10 | ADD COLUMN "contributionDescription" TEXT, 11 | ADD COLUMN "impactDescription" TEXT NOT NULL; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20231103121224_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserAttestation" ( 3 | "user_id" INTEGER NOT NULL, 4 | "collection_id" INTEGER NOT NULL, 5 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "UserAttestation_pkey" PRIMARY KEY ("user_id","collection_id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "UserAttestation" ADD CONSTRAINT "UserAttestation_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "UserAttestation" ADD CONSTRAINT "UserAttestation_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20231105104250_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "metadataUrl" TEXT, 3 | ALTER COLUMN "url" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240423124828_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `collection1Id` on the `ExpertiseVote` table. All the data in the column will be lost. 5 | - You are about to drop the column `collection2Id` on the `ExpertiseVote` table. All the data in the column will be lost. 6 | - You are about to drop the column `createdAt` on the `ExpertiseVote` table. All the data in the column will be lost. 7 | - You are about to drop the column `pickedId` on the `ExpertiseVote` table. All the data in the column will be lost. 8 | - You are about to drop the column `updatedAt` on the `ExpertiseVote` table. All the data in the column will be lost. 9 | - You are about to drop the column `userId` on the `ExpertiseVote` table. All the data in the column will be lost. 10 | - You are about to drop the column `createdAt` on the `Nonce` table. All the data in the column will be lost. 11 | - You are about to drop the column `expiresAt` on the `Nonce` table. All the data in the column will be lost. 12 | - You are about to drop the column `userId` on the `Nonce` table. All the data in the column will be lost. 13 | - You are about to drop the column `createdAt` on the `Poll` table. All the data in the column will be lost. 14 | - You are about to drop the column `endsAt` on the `Poll` table. All the data in the column will be lost. 15 | - You are about to drop the column `spaceId` on the `Poll` table. All the data in the column will be lost. 16 | - You are about to drop the column `createdAt` on the `Project` table. All the data in the column will be lost. 17 | - You are about to drop the column `pollId` on the `Project` table. All the data in the column will be lost. 18 | - The primary key for the `Share` table will be changed. If it partially fails, the table could be left without primary key constraint. 19 | - You are about to drop the column `createdAt` on the `Share` table. All the data in the column will be lost. 20 | - You are about to drop the column `projectId` on the `Share` table. All the data in the column will be lost. 21 | - You are about to drop the column `updatedAt` on the `Share` table. All the data in the column will be lost. 22 | - You are about to drop the column `userId` on the `Share` table. All the data in the column will be lost. 23 | - You are about to drop the column `createdAt` on the `Space` table. All the data in the column will be lost. 24 | - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost. 25 | - You are about to drop the column `isBadgeHolder` on the `User` table. All the data in the column will be lost. 26 | - The primary key for the `UserAttestation` table will be changed. If it partially fails, the table could be left without primary key constraint. 27 | - You are about to drop the column `collectionId` on the `UserAttestation` table. All the data in the column will be lost. 28 | - You are about to drop the column `createdAt` on the `UserAttestation` table. All the data in the column will be lost. 29 | - You are about to drop the column `updatedAt` on the `UserAttestation` table. All the data in the column will be lost. 30 | - You are about to drop the column `userId` on the `UserAttestation` table. All the data in the column will be lost. 31 | - The primary key for the `UserCollectionFinish` table will be changed. If it partially fails, the table could be left without primary key constraint. 32 | - You are about to drop the column `collectionId` on the `UserCollectionFinish` table. All the data in the column will be lost. 33 | - You are about to drop the column `createdAt` on the `UserCollectionFinish` table. All the data in the column will be lost. 34 | - You are about to drop the column `updatedAt` on the `UserCollectionFinish` table. All the data in the column will be lost. 35 | - You are about to drop the column `userId` on the `UserCollectionFinish` table. All the data in the column will be lost. 36 | - You are about to drop the column `createdAt` on the `Vote` table. All the data in the column will be lost. 37 | - You are about to drop the column `pickedId` on the `Vote` table. All the data in the column will be lost. 38 | - You are about to drop the column `project1Id` on the `Vote` table. All the data in the column will be lost. 39 | - You are about to drop the column `project2Id` on the `Vote` table. All the data in the column will be lost. 40 | - You are about to drop the column `updatedAt` on the `Vote` table. All the data in the column will be lost. 41 | - You are about to drop the column `userId` on the `Vote` table. All the data in the column will be lost. 42 | - A unique constraint covering the columns `[collection1_id,collection2_id,user_id]` on the table `ExpertiseVote` will be added. If there are existing duplicate values, this will fail. 43 | - A unique constraint covering the columns `[user_id]` on the table `Nonce` will be added. If there are existing duplicate values, this will fail. 44 | - A unique constraint covering the columns `[project1_id,project2_id,user_id]` on the table `Vote` will be added. If there are existing duplicate values, this will fail. 45 | - Added the required column `collection1_id` to the `ExpertiseVote` table without a default value. This is not possible if the table is not empty. 46 | - Added the required column `collection2_id` to the `ExpertiseVote` table without a default value. This is not possible if the table is not empty. 47 | - Added the required column `user_id` to the `ExpertiseVote` table without a default value. This is not possible if the table is not empty. 48 | - Added the required column `expires_at` to the `Nonce` table without a default value. This is not possible if the table is not empty. 49 | - Added the required column `ends_at` to the `Poll` table without a default value. This is not possible if the table is not empty. 50 | - Added the required column `space_id` to the `Poll` table without a default value. This is not possible if the table is not empty. 51 | - Added the required column `poll_id` to the `Project` table without a default value. This is not possible if the table is not empty. 52 | - Added the required column `project_id` to the `Share` table without a default value. This is not possible if the table is not empty. 53 | - Added the required column `user_id` to the `Share` table without a default value. This is not possible if the table is not empty. 54 | - Added the required column `is_badgeholder` to the `User` table without a default value. This is not possible if the table is not empty. 55 | - Added the required column `collection_id` to the `UserAttestation` table without a default value. This is not possible if the table is not empty. 56 | - Added the required column `user_id` to the `UserAttestation` table without a default value. This is not possible if the table is not empty. 57 | - Added the required column `collection_id` to the `UserCollectionFinish` table without a default value. This is not possible if the table is not empty. 58 | - Added the required column `user_id` to the `UserCollectionFinish` table without a default value. This is not possible if the table is not empty. 59 | - Added the required column `project1_id` to the `Vote` table without a default value. This is not possible if the table is not empty. 60 | - Added the required column `project2_id` to the `Vote` table without a default value. This is not possible if the table is not empty. 61 | - Added the required column `user_id` to the `Vote` table without a default value. This is not possible if the table is not empty. 62 | 63 | */ 64 | -- DropForeignKey 65 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection1Id_fkey"; 66 | 67 | -- DropForeignKey 68 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection2Id_fkey"; 69 | 70 | -- DropForeignKey 71 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_pickedId_fkey"; 72 | 73 | -- DropForeignKey 74 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_userId_fkey"; 75 | 76 | -- DropForeignKey 77 | ALTER TABLE "Nonce" DROP CONSTRAINT "Nonce_userId_fkey"; 78 | 79 | -- DropForeignKey 80 | ALTER TABLE "Poll" DROP CONSTRAINT "Poll_spaceId_fkey"; 81 | 82 | -- DropForeignKey 83 | ALTER TABLE "Project" DROP CONSTRAINT "Project_pollId_fkey"; 84 | 85 | -- DropForeignKey 86 | ALTER TABLE "Share" DROP CONSTRAINT "Share_projectId_fkey"; 87 | 88 | -- DropForeignKey 89 | ALTER TABLE "Share" DROP CONSTRAINT "Share_userId_fkey"; 90 | 91 | -- DropForeignKey 92 | ALTER TABLE "UserAttestation" DROP CONSTRAINT "UserAttestation_collectionId_fkey"; 93 | 94 | -- DropForeignKey 95 | ALTER TABLE "UserAttestation" DROP CONSTRAINT "UserAttestation_userId_fkey"; 96 | 97 | -- DropForeignKey 98 | ALTER TABLE "UserCollectionFinish" DROP CONSTRAINT "UserCollectionFinish_collectionId_fkey"; 99 | 100 | -- DropForeignKey 101 | ALTER TABLE "UserCollectionFinish" DROP CONSTRAINT "UserCollectionFinish_userId_fkey"; 102 | 103 | -- DropForeignKey 104 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_pickedId_fkey"; 105 | 106 | -- DropForeignKey 107 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_project1Id_fkey"; 108 | 109 | -- DropForeignKey 110 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_project2Id_fkey"; 111 | 112 | -- DropForeignKey 113 | ALTER TABLE "Vote" DROP CONSTRAINT "Vote_userId_fkey"; 114 | 115 | -- DropIndex 116 | DROP INDEX "ExpertiseVote_collection1Id_collection2Id_userId_key"; 117 | 118 | -- DropIndex 119 | DROP INDEX "Nonce_userId_key"; 120 | 121 | -- DropIndex 122 | DROP INDEX "Vote_project1Id_project2Id_userId_key"; 123 | 124 | -- AlterTable 125 | ALTER TABLE "ExpertiseVote" DROP COLUMN "collection1Id", 126 | DROP COLUMN "collection2Id", 127 | DROP COLUMN "createdAt", 128 | DROP COLUMN "pickedId", 129 | DROP COLUMN "updatedAt", 130 | DROP COLUMN "userId", 131 | ADD COLUMN "collection1_id" INTEGER NOT NULL, 132 | ADD COLUMN "collection2_id" INTEGER NOT NULL, 133 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 134 | ADD COLUMN "picked_id" INTEGER, 135 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 136 | ADD COLUMN "user_id" INTEGER NOT NULL; 137 | 138 | -- AlterTable 139 | ALTER TABLE "Nonce" DROP COLUMN "createdAt", 140 | DROP COLUMN "expiresAt", 141 | DROP COLUMN "userId", 142 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 143 | ADD COLUMN "expires_at" TEXT NOT NULL, 144 | ADD COLUMN "user_id" INTEGER; 145 | 146 | -- AlterTable 147 | ALTER TABLE "Poll" DROP COLUMN "createdAt", 148 | DROP COLUMN "endsAt", 149 | DROP COLUMN "spaceId", 150 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 151 | ADD COLUMN "ends_at" TIMESTAMP(3) NOT NULL, 152 | ADD COLUMN "space_id" INTEGER NOT NULL; 153 | 154 | -- AlterTable 155 | ALTER TABLE "Project" DROP COLUMN "createdAt", 156 | DROP COLUMN "pollId", 157 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 158 | ADD COLUMN "poll_id" INTEGER NOT NULL; 159 | 160 | -- AlterTable 161 | ALTER TABLE "Share" DROP CONSTRAINT "Share_pkey", 162 | DROP COLUMN "createdAt", 163 | DROP COLUMN "projectId", 164 | DROP COLUMN "updatedAt", 165 | DROP COLUMN "userId", 166 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 167 | ADD COLUMN "project_id" INTEGER NOT NULL, 168 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 169 | ADD COLUMN "user_id" INTEGER NOT NULL, 170 | ADD CONSTRAINT "Share_pkey" PRIMARY KEY ("user_id", "project_id"); 171 | 172 | -- AlterTable 173 | ALTER TABLE "Space" DROP COLUMN "createdAt", 174 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 175 | 176 | -- AlterTable 177 | ALTER TABLE "User" DROP COLUMN "createdAt", 178 | DROP COLUMN "isBadgeHolder", 179 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 180 | ADD COLUMN "is_badgeholder" INTEGER NOT NULL; 181 | 182 | -- AlterTable 183 | ALTER TABLE "UserAttestation" DROP CONSTRAINT "UserAttestation_pkey", 184 | DROP COLUMN "collectionId", 185 | DROP COLUMN "createdAt", 186 | DROP COLUMN "updatedAt", 187 | DROP COLUMN "userId", 188 | ADD COLUMN "collection_id" INTEGER NOT NULL, 189 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 190 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 191 | ADD COLUMN "user_id" INTEGER NOT NULL, 192 | ADD CONSTRAINT "UserAttestation_pkey" PRIMARY KEY ("user_id", "collection_id"); 193 | 194 | -- AlterTable 195 | ALTER TABLE "UserCollectionFinish" DROP CONSTRAINT "UserCollectionFinish_pkey", 196 | DROP COLUMN "collectionId", 197 | DROP COLUMN "createdAt", 198 | DROP COLUMN "updatedAt", 199 | DROP COLUMN "userId", 200 | ADD COLUMN "collection_id" INTEGER NOT NULL, 201 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 202 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 203 | ADD COLUMN "user_id" INTEGER NOT NULL, 204 | ADD CONSTRAINT "UserCollectionFinish_pkey" PRIMARY KEY ("user_id", "collection_id"); 205 | 206 | -- AlterTable 207 | ALTER TABLE "Vote" DROP COLUMN "createdAt", 208 | DROP COLUMN "pickedId", 209 | DROP COLUMN "project1Id", 210 | DROP COLUMN "project2Id", 211 | DROP COLUMN "updatedAt", 212 | DROP COLUMN "userId", 213 | ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 214 | ADD COLUMN "picked_id" INTEGER, 215 | ADD COLUMN "project1_id" INTEGER NOT NULL, 216 | ADD COLUMN "project2_id" INTEGER NOT NULL, 217 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 218 | ADD COLUMN "user_id" INTEGER NOT NULL; 219 | 220 | -- CreateIndex 221 | CREATE UNIQUE INDEX "ExpertiseVote_collection1_id_collection2_id_user_id_key" ON "ExpertiseVote"("collection1_id", "collection2_id", "user_id"); 222 | 223 | -- CreateIndex 224 | CREATE UNIQUE INDEX "Nonce_user_id_key" ON "Nonce"("user_id"); 225 | 226 | -- CreateIndex 227 | CREATE UNIQUE INDEX "Vote_project1_id_project2_id_user_id_key" ON "Vote"("project1_id", "project2_id", "user_id"); 228 | 229 | -- AddForeignKey 230 | ALTER TABLE "Poll" ADD CONSTRAINT "Poll_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "Space"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 231 | 232 | -- AddForeignKey 233 | ALTER TABLE "Project" ADD CONSTRAINT "Project_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "Poll"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 234 | 235 | -- AddForeignKey 236 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 237 | 238 | -- AddForeignKey 239 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project1_id_fkey" FOREIGN KEY ("project1_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 240 | 241 | -- AddForeignKey 242 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_project2_id_fkey" FOREIGN KEY ("project2_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 243 | 244 | -- AddForeignKey 245 | ALTER TABLE "Vote" ADD CONSTRAINT "Vote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 246 | 247 | -- AddForeignKey 248 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 249 | 250 | -- AddForeignKey 251 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection1_id_fkey" FOREIGN KEY ("collection1_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 252 | 253 | -- AddForeignKey 254 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_collection2_id_fkey" FOREIGN KEY ("collection2_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 255 | 256 | -- AddForeignKey 257 | ALTER TABLE "ExpertiseVote" ADD CONSTRAINT "ExpertiseVote_picked_id_fkey" FOREIGN KEY ("picked_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 258 | 259 | -- AddForeignKey 260 | ALTER TABLE "Nonce" ADD CONSTRAINT "Nonce_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 261 | 262 | -- AddForeignKey 263 | ALTER TABLE "Share" ADD CONSTRAINT "Share_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 264 | 265 | -- AddForeignKey 266 | ALTER TABLE "Share" ADD CONSTRAINT "Share_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 267 | 268 | -- AddForeignKey 269 | ALTER TABLE "UserCollectionFinish" ADD CONSTRAINT "UserCollectionFinish_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 270 | 271 | -- AddForeignKey 272 | ALTER TABLE "UserCollectionFinish" ADD CONSTRAINT "UserCollectionFinish_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 273 | 274 | -- AddForeignKey 275 | ALTER TABLE "UserAttestation" ADD CONSTRAINT "UserAttestation_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 276 | 277 | -- AddForeignKey 278 | ALTER TABLE "UserAttestation" ADD CONSTRAINT "UserAttestation_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 279 | -------------------------------------------------------------------------------- /prisma/migrations/20240423125156_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `RPGF3Id` on the `Project` table. All the data in the column will be lost. 5 | - You are about to drop the column `contributionDescription` on the `Project` table. All the data in the column will be lost. 6 | - You are about to drop the column `impactDescription` on the `Project` table. All the data in the column will be lost. 7 | - You are about to drop the column `metadataUrl` on the `Project` table. All the data in the column will be lost. 8 | - You are about to drop the column `parentId` on the `Project` table. All the data in the column will be lost. 9 | - Added the required column `impact_description` to the `Project` table without a default value. This is not possible if the table is not empty. 10 | 11 | */ 12 | -- DropForeignKey 13 | ALTER TABLE "Project" DROP CONSTRAINT "Project_parentId_fkey"; 14 | 15 | -- AlterTable 16 | ALTER TABLE "Project" DROP COLUMN "RPGF3Id", 17 | DROP COLUMN "contributionDescription", 18 | DROP COLUMN "impactDescription", 19 | DROP COLUMN "metadataUrl", 20 | DROP COLUMN "parentId", 21 | ADD COLUMN "contribution_description" TEXT, 22 | ADD COLUMN "impact_description" TEXT NOT NULL, 23 | ADD COLUMN "metadata_url" TEXT, 24 | ADD COLUMN "parent_id" INTEGER, 25 | ADD COLUMN "rpgf4_id" TEXT; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Project" ADD CONSTRAINT "Project_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /prisma/migrations/20240430104145_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ProjectExclusion" ( 3 | "user_id" INTEGER NOT NULL, 4 | "project_id" INTEGER NOT NULL, 5 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "ProjectExclusion_pkey" PRIMARY KEY ("user_id","project_id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "UserCollectionFiltered" ( 13 | "user_id" INTEGER NOT NULL, 14 | "collection_id" INTEGER NOT NULL, 15 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | 18 | CONSTRAINT "UserCollectionFiltered_pkey" PRIMARY KEY ("user_id","collection_id") 19 | ); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "ProjectExclusion" ADD CONSTRAINT "ProjectExclusion_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "ProjectExclusion" ADD CONSTRAINT "ProjectExclusion_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "UserCollectionFiltered" ADD CONSTRAINT "UserCollectionFiltered_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "UserCollectionFiltered" ADD CONSTRAINT "UserCollectionFiltered_collection_id_fkey" FOREIGN KEY ("collection_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 32 | -------------------------------------------------------------------------------- /prisma/migrations/20240430160526_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `ProjectExclusion` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- CreateEnum 8 | CREATE TYPE "InclusionState" AS ENUM ('included', 'excluded'); 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "ProjectExclusion" DROP CONSTRAINT "ProjectExclusion_project_id_fkey"; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "ProjectExclusion" DROP CONSTRAINT "ProjectExclusion_user_id_fkey"; 15 | 16 | -- DropTable 17 | DROP TABLE "ProjectExclusion"; 18 | 19 | -- CreateTable 20 | CREATE TABLE "ProjectInclusion" ( 21 | "user_id" INTEGER NOT NULL, 22 | "project_id" INTEGER NOT NULL, 23 | "state" "InclusionState" NOT NULL, 24 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 25 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 | 27 | CONSTRAINT "ProjectInclusion_pkey" PRIMARY KEY ("user_id","project_id") 28 | ); 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "ProjectInclusion" ADD CONSTRAINT "ProjectInclusion_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "ProjectInclusion" ADD CONSTRAINT "ProjectInclusion_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /prisma/migrations/20240518191441_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[address]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateTable 8 | CREATE TABLE "Otp" ( 9 | "user_id" INTEGER NOT NULL, 10 | "otp" TEXT NOT NULL, 11 | "expires_at" TEXT NOT NULL, 12 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | 14 | CONSTRAINT "Otp_pkey" PRIMARY KEY ("user_id","otp") 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Otp_user_id_key" ON "Otp"("user_id"); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "User_address_key" ON "User"("address"); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "Otp" ADD CONSTRAINT "Otp_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20240603141921_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "badges" JSONB, 3 | ADD COLUMN "identity" JSONB; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240610150253_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[identity]` on the table `User` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "User_identity_key" ON "User"("identity"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240617170023_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Share` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Share" DROP CONSTRAINT "Share_project_id_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "Share" DROP CONSTRAINT "Share_user_id_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "Share"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "Rank" ( 18 | "user_id" INTEGER NOT NULL, 19 | "project_id" INTEGER NOT NULL, 20 | "rank" INTEGER, 21 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | 24 | CONSTRAINT "Rank_pkey" PRIMARY KEY ("user_id","project_id") 25 | ); 26 | 27 | -- CreateIndex 28 | CREATE UNIQUE INDEX "Rank_user_id_project_id_rank_key" ON "Rank"("user_id", "project_id", "rank"); 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "Rank" ADD CONSTRAINT "Rank_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "Rank" ADD CONSTRAINT "Rank_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /prisma/migrations/20240618122713_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "metrics_project_id" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240627093345_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `ExpertiseVote` table. If the table is not empty, all the data it contains will be lost. 5 | - A unique constraint covering the columns `[op_address]` on the table `User` will be added. If there are existing duplicate values, this will fail. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection1_id_fkey"; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_collection2_id_fkey"; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_picked_id_fkey"; 16 | 17 | -- DropForeignKey 18 | ALTER TABLE "ExpertiseVote" DROP CONSTRAINT "ExpertiseVote_user_id_fkey"; 19 | 20 | -- AlterTable 21 | ALTER TABLE "User" ADD COLUMN "op_address" TEXT, 22 | ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 23 | 24 | -- DropTable 25 | DROP TABLE "ExpertiseVote"; 26 | 27 | -- CreateIndex 28 | CREATE UNIQUE INDEX "User_op_address_key" ON "User"("op_address"); 29 | -------------------------------------------------------------------------------- /prisma/migrations/20240628084417_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "short_description" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240630094629_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "User_identity_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240815150505_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Otp` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `ProjectInclusion` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `UserAttestation` table. If the table is not empty, all the data it contains will be lost. 7 | - You are about to drop the `UserCollectionFiltered` table. If the table is not empty, all the data it contains will be lost. 8 | 9 | */ 10 | -- DropForeignKey 11 | ALTER TABLE "Otp" DROP CONSTRAINT "Otp_user_id_fkey"; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "ProjectInclusion" DROP CONSTRAINT "ProjectInclusion_project_id_fkey"; 15 | 16 | -- DropForeignKey 17 | ALTER TABLE "ProjectInclusion" DROP CONSTRAINT "ProjectInclusion_user_id_fkey"; 18 | 19 | -- DropForeignKey 20 | ALTER TABLE "UserAttestation" DROP CONSTRAINT "UserAttestation_collection_id_fkey"; 21 | 22 | -- DropForeignKey 23 | ALTER TABLE "UserAttestation" DROP CONSTRAINT "UserAttestation_user_id_fkey"; 24 | 25 | -- DropForeignKey 26 | ALTER TABLE "UserCollectionFiltered" DROP CONSTRAINT "UserCollectionFiltered_collection_id_fkey"; 27 | 28 | -- DropForeignKey 29 | ALTER TABLE "UserCollectionFiltered" DROP CONSTRAINT "UserCollectionFiltered_user_id_fkey"; 30 | 31 | -- DropTable 32 | DROP TABLE "Otp"; 33 | 34 | -- DropTable 35 | DROP TABLE "ProjectInclusion"; 36 | 37 | -- DropTable 38 | DROP TABLE "UserAttestation"; 39 | 40 | -- DropTable 41 | DROP TABLE "UserCollectionFiltered"; 42 | 43 | -- DropEnum 44 | DROP TYPE "InclusionState"; 45 | 46 | -- CreateTable 47 | CREATE TABLE "ProjectStar" ( 48 | "user_id" INTEGER NOT NULL, 49 | "project_id" INTEGER NOT NULL, 50 | "star" INTEGER NOT NULL, 51 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 52 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 53 | 54 | CONSTRAINT "ProjectStar_pkey" PRIMARY KEY ("user_id","project_id") 55 | ); 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "ProjectStar" ADD CONSTRAINT "ProjectStar_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "ProjectStar" ADD CONSTRAINT "ProjectStar_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | -------------------------------------------------------------------------------- /prisma/migrations/20240915094957_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `contribution_description` on the `Project` table. All the data in the column will be lost. 5 | - You are about to drop the column `impact_description` on the `Project` table. All the data in the column will be lost. 6 | - You are about to drop the column `metadata_url` on the `Project` table. All the data in the column will be lost. 7 | - You are about to drop the column `metrics_project_id` on the `Project` table. All the data in the column will be lost. 8 | - Added the required column `description` to the `Project` table without a default value. This is not possible if the table is not empty. 9 | - Added the required column `metadata` to the `Project` table without a default value. This is not possible if the table is not empty. 10 | 11 | */ 12 | -- AlterTable 13 | ALTER TABLE "Project" DROP COLUMN "contribution_description", 14 | DROP COLUMN "impact_description", 15 | DROP COLUMN "metadata_url", 16 | DROP COLUMN "metrics_project_id", 17 | ADD COLUMN "description" TEXT NOT NULL, 18 | ADD COLUMN "metadata" TEXT NOT NULL; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20240915173005_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `ProjectStar` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ProjectStar" DROP CONSTRAINT "ProjectStar_pkey", 9 | ADD COLUMN "id" SERIAL NOT NULL, 10 | ADD CONSTRAINT "ProjectStar_pkey" PRIMARY KEY ("id"); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20240915191834_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ProjectCoI" ( 3 | "user_id" INTEGER NOT NULL, 4 | "project_id" INTEGER NOT NULL, 5 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "ProjectCoI_pkey" PRIMARY KEY ("user_id","project_id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "ProjectCoI" ADD CONSTRAINT "ProjectCoI_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "ProjectCoI" ADD CONSTRAINT "ProjectCoI_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20240920095026_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "implicit-category" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240928110832_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "ai_summary" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241003194312_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "ballot_success" INTEGER; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling 4 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection 5 | } 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | address String @unique() 14 | ballotSuccess Int? @map("ballot_success") 15 | opAddress String? @unique() @map("op_address") 16 | isBadgeHolder Int @map("is_badgeholder") 17 | createdAt DateTime @default(now()) @map("created_at") 18 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 19 | identity Json? // Ideally should be unique except {} and null values but Prisma doesn't support partial 20 | // unique constraints 21 | badges Json? 22 | projectVotes Vote[] 23 | nonce Nonce? 24 | finishedCollection UserCollectionFinish[] 25 | ranks Rank[] 26 | projectStars ProjectStar[] 27 | cois ProjectCoI[] 28 | } 29 | 30 | model Space { 31 | id Int @id @default(autoincrement()) 32 | title String 33 | description String 34 | createdAt DateTime @default(now()) @map("created_at") 35 | polls Poll[] 36 | } 37 | 38 | enum PollStatus { 39 | ACTIVE 40 | CLOSED 41 | } 42 | 43 | model Poll { 44 | id Int @id @default(autoincrement()) 45 | title String 46 | spaceId Int @map("space_id") 47 | status PollStatus @default(ACTIVE) 48 | endsAt DateTime @map("ends_at") 49 | createdAt DateTime @default(now()) @map("created_at") 50 | space Space @relation(fields: [spaceId], references: [id]) 51 | projects Project[] 52 | } 53 | 54 | enum ProjectType { 55 | collection 56 | compositeProject 57 | project 58 | } 59 | 60 | model Project { 61 | id Int @id @default(autoincrement()) 62 | name String 63 | pollId Int @map("poll_id") 64 | url String? 65 | description String 66 | implicitCategory String? @map("implicit-category") 67 | shortDescription String? @map("short_description") 68 | RPGF5Id String? @map("rpgf4_id") 69 | parentId Int? @map("parent_id") 70 | image String? 71 | metadata String 72 | aiSummary String? @map("ai_summary") 73 | createdAt DateTime @default(now()) @map("created_at") 74 | parent Project? @relation("ParentRelation", fields: [parentId], references: [id]) 75 | children Project[] @relation("ParentRelation") 76 | type ProjectType 77 | poll Poll @relation(fields: [pollId], references: [id]) 78 | options1 Vote[] @relation("VoteToProject1") 79 | options2 Vote[] @relation("VoteToProject2") 80 | voted Vote[] @relation("Voted") 81 | ranks Rank[] 82 | finishedCollections UserCollectionFinish[] 83 | stars ProjectStar[] 84 | cois ProjectCoI[] 85 | } 86 | 87 | model Vote { 88 | id Int @id @default(autoincrement()) 89 | userId Int @map("user_id") 90 | project1Id Int @map("project1_id") 91 | project2Id Int @map("project2_id") 92 | pickedId Int? @map("picked_id") 93 | createdAt DateTime @default(now()) @map("created_at") 94 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 95 | user User @relation(fields: [userId], references: [id]) 96 | project1 Project @relation("VoteToProject1", fields: [project1Id], references: [id]) 97 | project2 Project @relation("VoteToProject2", fields: [project2Id], references: [id]) 98 | picked Project? @relation("Voted", fields: [pickedId], references: [id]) 99 | 100 | @@unique([project1Id, project2Id, userId]) 101 | } 102 | 103 | model Nonce { 104 | id Int @id @default(autoincrement()) 105 | userId Int? @unique() @map("user_id") 106 | nonce String 107 | expiresAt String @map("expires_at") 108 | createdAt DateTime @default(now()) @map("created_at") 109 | user User? @relation(fields: [userId], references: [id]) 110 | } 111 | 112 | // model Otp { 113 | // userId Int @unique() @map("user_id") 114 | // otp String 115 | // expiresAt String @map("expires_at") 116 | // createdAt DateTime @default(now()) @map("created_at") 117 | // user User @relation(fields: [userId], references: [id]) 118 | 119 | // @@id([userId, otp]) 120 | // } 121 | 122 | // model Share { 123 | // userId Int @map("user_id") 124 | // projectId Int @map("project_id") 125 | // share Float // the percentage of allocation from the 100% available to all projects 126 | // createdAt DateTime @default(now()) @map("created_at") 127 | // updatedAt DateTime @default(now()) @map("updated_at") 128 | // user User @relation(fields: [userId], references: [id]) 129 | // project Project @relation(fields: [projectId], references: [id]) 130 | 131 | // @@id([userId, projectId]) 132 | // } 133 | 134 | model Rank { 135 | userId Int @map("user_id") 136 | projectId Int @map("project_id") 137 | rank Int? // the rank of a project in a category (rank = 1 is the best, etc) 138 | createdAt DateTime @default(now()) @map("created_at") 139 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 140 | user User @relation(fields: [userId], references: [id]) 141 | project Project @relation(fields: [projectId], references: [id]) 142 | 143 | @@id([userId, projectId]) 144 | @@unique([userId, projectId, rank]) 145 | } 146 | 147 | model ProjectStar { 148 | id Int @id @default(autoincrement()) 149 | userId Int @map("user_id") 150 | projectId Int @map("project_id") 151 | star Int 152 | createdAt DateTime @default(now()) @map("created_at") 153 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 154 | user User @relation(fields: [userId], references: [id]) 155 | project Project @relation(fields: [projectId], references: [id]) 156 | } 157 | 158 | model ProjectCoI { 159 | userId Int @map("user_id") 160 | projectId Int @map("project_id") 161 | createdAt DateTime @default(now()) @map("created_at") 162 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 163 | user User @relation(fields: [userId], references: [id]) 164 | project Project @relation(fields: [projectId], references: [id]) 165 | 166 | @@id([userId, projectId]) 167 | } 168 | 169 | model UserCollectionFinish { 170 | userId Int @map("user_id") 171 | collectionId Int @map("collection_id") 172 | createdAt DateTime @default(now()) @map("created_at") 173 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 174 | user User @relation(fields: [userId], references: [id]) 175 | collection Project @relation(fields: [collectionId], references: [id]) 176 | 177 | @@id([userId, collectionId]) 178 | } 179 | 180 | // model UserAttestation { 181 | // userId Int @map("user_id") 182 | // collectionId Int @map("collection_id") 183 | // createdAt DateTime @default(now()) @map("created_at") 184 | // updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 185 | // user User @relation(fields: [userId], references: [id]) 186 | // collection Project @relation(fields: [collectionId], references: [id]) 187 | 188 | // @@id([userId, collectionId]) 189 | // } 190 | -------------------------------------------------------------------------------- /prisma/seeds/pwcat.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeneralMagicio/pw-backend/530d6a1703b212bffe7c69fb93cb69223fa60cda/prisma/seeds/pwcat.xlsx -------------------------------------------------------------------------------- /src/analytics/analytics.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger, Res } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma.service'; 3 | import { Response } from 'express'; 4 | import { BadgeData } from 'src/utils/badges/readBadges'; 5 | 6 | @Controller({ path: 'analytics' }) 7 | export class AnalyticsController { 8 | private readonly logger = new Logger(AnalyticsController.name); 9 | // private badgeholders = BHs.map((item) => item.recipient); 10 | constructor(private readonly prismaService: PrismaService) {} 11 | 12 | // @Get('summary') 13 | // async countTotalUsers(@Res() res: Response) { 14 | // const users = await this.prismaService.user.findMany({}); 15 | 16 | // console.log('Total # of users', users.length); 17 | 18 | // const sHTemp = users.filter( 19 | // (user) => 20 | // user.badges && 21 | // user.identity && 22 | // typeof user.badges === 'object' && 23 | // Object.keys(user.badges).length > 0, 24 | // ); 25 | 26 | // console.log('Total # of stakeholders:', sHTemp.length); 27 | 28 | // const guests = users.filter( 29 | // (user) => 30 | // user.badges && 31 | // user.identity && 32 | // typeof user.badges === 'object' && 33 | // typeof user.identity === 'object' && 34 | // Object.keys(user.badges).length === 0 && 35 | // Object.keys(user.identity).length === 0, 36 | // ); 37 | 38 | // console.log( 39 | // 'Total # of guests (No wallet connected but interacted with the app):', 40 | // guests.length, 41 | // ); 42 | 43 | // const noInteractions = users.filter( 44 | // (user) => !user.badges && !user.identity, 45 | // ); 46 | 47 | // console.log( 48 | // 'Total # of users that had no interaction after logging:', 49 | // noInteractions.length, 50 | // ); 51 | 52 | // const stakeholders = sHTemp.map(({ badges, ...el }) => ({ 53 | // ...el, 54 | // badges: badges?.valueOf() as BadgeData, 55 | // })); 56 | 57 | // const recipients = stakeholders.filter( 58 | // (el) => el.badges.recipientsPoints && el.badges.recipientsPoints > 0, 59 | // ); 60 | 61 | // const holders = stakeholders.filter( 62 | // (el) => el.badges.holderPoints && el.badges.holderPoints > 0, 63 | // ); 64 | 65 | // const badgeHolders = stakeholders.filter( 66 | // (el) => el.badges.badgeholderPoints && el.badges.badgeholderPoints > 0, 67 | // ); 68 | 69 | // const delegates = stakeholders.filter( 70 | // (el) => el.badges.delegatePoints && el.badges.delegatePoints > 0, 71 | // ); 72 | 73 | // const recipientAttests = await this.prismaService.userAttestation.findMany({ 74 | // where: { 75 | // userId: { in: recipients.map((el) => el.id) }, 76 | // }, 77 | // }); 78 | 79 | // const holderAttestations = 80 | // await this.prismaService.userAttestation.findMany({ 81 | // where: { 82 | // userId: { in: holders.map((el) => el.id) }, 83 | // }, 84 | // }); 85 | 86 | // const badgeHolderAttestations = 87 | // await this.prismaService.userAttestation.findMany({ 88 | // where: { 89 | // userId: { in: badgeHolders.map((el) => el.id) }, 90 | // }, 91 | // }); 92 | 93 | // const delegateAttestations = 94 | // await this.prismaService.userAttestation.findMany({ 95 | // where: { 96 | // userId: { in: delegates.map((el) => el.id) }, 97 | // }, 98 | // }); 99 | 100 | // const recipientCategories = 101 | // await this.prismaService.userCollectionFinish.findMany({ 102 | // where: { userId: { in: recipients.map((el) => el.id) } }, 103 | // }); 104 | 105 | // const holderCategories = 106 | // await this.prismaService.userCollectionFinish.findMany({ 107 | // where: { userId: { in: holders.map((el) => el.id) } }, 108 | // }); 109 | 110 | // const badgeHolderCategories = 111 | // await this.prismaService.userCollectionFinish.findMany({ 112 | // where: { userId: { in: badgeHolders.map((el) => el.id) } }, 113 | // }); 114 | 115 | // const delegateCategories = 116 | // await this.prismaService.userCollectionFinish.findMany({ 117 | // where: { userId: { in: delegates.map((el) => el.id) } }, 118 | // }); 119 | 120 | // console.log('Number of users with the recipient badge:', recipients.length); 121 | // console.log('Number of category completed:', recipientCategories.length); 122 | // console.log('Number of their votes:', recipientAttests.length); 123 | 124 | // console.log(); 125 | 126 | // console.log('Number of users with the holder badge:', holders.length); 127 | // console.log('Number of category completed:', holderCategories.length); 128 | // console.log('Number of their votes:', holderAttestations.length); 129 | 130 | // console.log(); 131 | 132 | // console.log( 133 | // 'Number of users with the badge-holder badge:', 134 | // badgeHolders.length, 135 | // ); 136 | // console.log('Number of category completed:', badgeHolderCategories.length); 137 | // console.log('Number of their votes:', badgeHolderAttestations.length); 138 | 139 | // console.log(); 140 | 141 | // console.log('Number of users with the delegate badge:', delegates.length); 142 | // console.log('Number of category completed:', delegateCategories.length); 143 | // console.log('Number of their votes:', delegateAttestations.length); 144 | 145 | // return 'Success'; 146 | // } 147 | 148 | // @Get('cat-sum') 149 | // async categoryBasedVotes(@Res() res: Response) { 150 | // const users = await this.prismaService.user.findMany({}); 151 | // const categories = await this.prismaService.project.findMany({ 152 | // select: { id: true, name: true }, 153 | // where: { type: 'collection' }, 154 | // }); 155 | 156 | // const sHTemp = users.filter( 157 | // (user) => 158 | // user.badges && 159 | // user.identity && 160 | // typeof user.badges === 'object' && 161 | // Object.keys(user.badges).length > 0, 162 | // ); 163 | 164 | // const stakeholders = sHTemp.map(({ badges, ...el }) => ({ 165 | // ...el, 166 | // badges: badges?.valueOf() as BadgeData, 167 | // })); 168 | 169 | // const recipients = stakeholders.filter( 170 | // (el) => el.badges.recipientsPoints && el.badges.recipientsPoints > 0, 171 | // ); 172 | 173 | // const holders = stakeholders.filter( 174 | // (el) => el.badges.holderPoints && el.badges.holderPoints > 0, 175 | // ); 176 | 177 | // const badgeHolders = stakeholders.filter( 178 | // (el) => el.badges.badgeholderPoints && el.badges.badgeholderPoints > 0, 179 | // ); 180 | 181 | // const delegates = stakeholders.filter( 182 | // (el) => el.badges.delegatePoints && el.badges.delegatePoints > 0, 183 | // ); 184 | 185 | // const targetDate = new Date(Date.UTC(2024, 6, 17, 14, 0, 0)); // Note: Months are 0-indexed in JavaScript Date 186 | 187 | // for (const category of categories) { 188 | // const votes = await this.prismaService.userAttestation.findMany({ 189 | // where: { 190 | // collectionId: category.id, 191 | // // createdAt: { lte: targetDate }, 192 | // }, 193 | // }); 194 | 195 | // console.log('Category:', category.name); 196 | // console.log('Total votes:', votes.length); 197 | // console.log( 198 | // `Votes from recipients ${ 199 | // votes.filter( 200 | // (el) => 201 | // recipients.findIndex((user) => user.id === el.userId) !== -1, 202 | // ).length 203 | // }`, 204 | // ); 205 | // console.log( 206 | // `Votes from holders ${ 207 | // votes.filter( 208 | // (el) => holders.findIndex((user) => user.id === el.userId) !== -1, 209 | // ).length 210 | // }`, 211 | // ); 212 | // console.log( 213 | // `Votes from badgeholders ${ 214 | // votes.filter( 215 | // (el) => 216 | // badgeHolders.findIndex((user) => user.id === el.userId) !== -1, 217 | // ).length 218 | // }`, 219 | // ); 220 | // console.log( 221 | // `Votes from delegates ${ 222 | // votes.filter( 223 | // (el) => delegates.findIndex((user) => user.id === el.userId) !== -1, 224 | // ).length 225 | // }`, 226 | // ); 227 | // console.log(); 228 | // } 229 | 230 | // return 'Success'; 231 | // } 232 | } 233 | -------------------------------------------------------------------------------- /src/analytics/analytics.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AnalyticsService } from './analytics.service'; 3 | import { AnalyticsController } from './analytics.controller'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | import { FlowModule } from 'src/flow/flow.module'; 6 | 7 | @Module({ 8 | imports: [FlowModule], 9 | providers: [AnalyticsService, PrismaService, AnalyticsController], 10 | controllers: [AnalyticsController], 11 | exports: [AnalyticsService], 12 | }) 13 | export class AnalyticsModule {} 14 | -------------------------------------------------------------------------------- /src/analytics/analytics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { FlowService } from 'src/flow/flow.service'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | 5 | @Injectable() 6 | export class AnalyticsService { 7 | private readonly logger = new Logger(AnalyticsService.name); 8 | constructor( 9 | private readonly prismaService: PrismaService, 10 | private readonly flowService: FlowService, 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { UsersModule } from './user/users.module'; 7 | import { MocksModule } from './mock/mock.module'; 8 | import { FlowModule } from './flow/flow.module'; 9 | import { CollectionModule } from './collection/colleciton.module'; 10 | import { AnalyticsModule } from './analytics/analytics.module'; 11 | import { ProjectModule } from './project/project.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot(), 16 | AuthModule, 17 | UsersModule, 18 | MocksModule, 19 | FlowModule, 20 | CollectionModule, 21 | ProjectModule, 22 | // AnalyticsModule, 23 | ], 24 | controllers: [AppController], 25 | providers: [AppService], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World! 2'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Logger, 7 | UnauthorizedException, 8 | Res, 9 | Req, 10 | UseGuards, 11 | UnprocessableEntityException, 12 | } from '@nestjs/common'; 13 | 14 | import { AuthService } from './auth.service'; 15 | import { PrismaService } from 'src/prisma.service'; 16 | import { Response } from 'express'; 17 | import { UsersService } from 'src/user/users.service'; 18 | import { AuthGuard } from './auth.guard'; 19 | import { LoginDTO } from './dto/login.dto'; 20 | import { ApiResponse } from '@nestjs/swagger'; 21 | import { AuthedReq } from 'src/utils/types/AuthedReq.type'; 22 | import { STAGING_API, generateRandomString } from 'src/utils'; 23 | import { FlowService } from 'src/flow/flow.service'; 24 | 25 | @Controller({ path: 'auth' }) 26 | export class AuthController { 27 | private readonly logger = new Logger(AuthController.name); 28 | constructor( 29 | private readonly authService: AuthService, 30 | private readonly prismaService: PrismaService, 31 | private readonly usersService: UsersService, 32 | private readonly flowService: FlowService, 33 | ) {} 34 | 35 | @UseGuards(AuthGuard) 36 | @ApiResponse({ 37 | status: 401, 38 | description: "You're not logged in", 39 | }) 40 | @ApiResponse({ 41 | status: 200, 42 | description: "You're logged in and the user object is returned", 43 | }) 44 | @Get('/isLoggedIn') 45 | async isLoggedIn(@Req() req: AuthedReq) { 46 | return req.userId; 47 | } 48 | 49 | @Post('/logout') 50 | async logout(@Res() res: Response) { 51 | // expire the token from the db because the expiration time of the tokens are rather long 52 | res.clearCookie('auth', { 53 | httpOnly: true, 54 | sameSite: process.env.NODE_ENV === 'staging' ? 'none' : 'lax', 55 | domain: process.env.NODE_ENV === 'development' ? undefined : STAGING_API, 56 | secure: true, 57 | }); 58 | res.send('Logged out.'); 59 | } 60 | 61 | @ApiResponse({ status: 200, description: 'Sets an auth cookie' }) 62 | @Post('/login') 63 | async login( 64 | @Res() res: Response, 65 | @Body() { message, signature, address }: LoginDTO, 66 | ) { 67 | let isNewUser = false; 68 | const isAuthentic = await this.authService.verifyUser( 69 | message, 70 | signature as `0x${string}`, 71 | address as `0x${string}`, 72 | ); 73 | if (!isAuthentic) throw new UnauthorizedException('Invalid signature'); 74 | 75 | let user = await this.prismaService.user.findFirst({ 76 | where: { address }, 77 | }); 78 | if (!user) { 79 | user = await this.usersService.create({ address, isBadgeHolder: true }); 80 | isNewUser = true; 81 | } 82 | 83 | if (!user) 84 | throw new UnprocessableEntityException('User can not be created'); 85 | 86 | await this.prismaService.nonce.deleteMany({ 87 | where: { 88 | userId: user.id, 89 | }, 90 | }); 91 | 92 | const token = generateRandomString({ 93 | length: 32, 94 | lowercase: true, 95 | numerical: true, 96 | uppercase: true, 97 | }); 98 | 99 | await this.prismaService.nonce.create({ 100 | data: { 101 | nonce: token, 102 | userId: user.id, 103 | expiresAt: `${Date.now() + this.authService.TokenExpirationDuration}`, 104 | }, 105 | }); 106 | 107 | const hasRanks = await this.prismaService.rank.findFirst({ 108 | where: { userId: user.id }, 109 | }); 110 | 111 | const noRanks = hasRanks === null; 112 | 113 | if (isNewUser || noRanks) 114 | await this.flowService.populateInitialRanking(user.id); 115 | // res.cookie('auth', nonce, { 116 | // httpOnly: true, 117 | // sameSite: process.env.NODE_ENV === 'staging' ? 'none' : 'lax', 118 | // domain: process.env.NODE_ENV === 'development' ? undefined : STAGING_API, 119 | // secure: true, 120 | // expires: new Date(Date.now() + this.authService.TokenExpirationDuration), 121 | // }); 122 | 123 | // return nonce; 124 | 125 | res.status(200).send({ token, isNewUser }); 126 | } 127 | 128 | @ApiResponse({ 129 | status: 200, 130 | type: String, 131 | description: 'a 48 character alphanumerical nonce is returned', 132 | }) 133 | @Get('/nonce') 134 | async getNonce() { 135 | const nonce = this.authService.generateNonce(); 136 | await this.prismaService.nonce.create({ 137 | data: { 138 | nonce, 139 | expiresAt: `${Date.now() + this.authService.NonceExpirationDuration}`, 140 | }, 141 | }); 142 | return nonce; 143 | } 144 | 145 | // @ApiResponse({ 146 | // status: 200, 147 | // type: String, 148 | // description: 'a 6 character numerical OTP is returned', 149 | // }) 150 | // @UseGuards(AuthGuard) 151 | // @Get('/otp') 152 | // async getOtp(@Req() { userId }: AuthedReq) { 153 | // const otp = await this.authService.assignOtp(userId); 154 | 155 | // return otp; 156 | // } 157 | 158 | // @ApiResponse({ 159 | // status: 200, 160 | // type: Boolean, 161 | // description: 'false or returning an auth token', 162 | // }) 163 | // @Post('/otp/validate') 164 | // async validateOtp(@Body() { otp }: OtpDTO) { 165 | // const userId = await this.authService.checkOtpValidity(otp); 166 | 167 | // if (!userId) throw new ForbiddenException('OTP invalid'); 168 | 169 | // const [nonce, _] = await Promise.all([ 170 | // this.prismaService.nonce.findUnique({ 171 | // where: { 172 | // userId: userId, 173 | // }, 174 | // }), 175 | // this.prismaService.otp.delete({ 176 | // where: { userId }, 177 | // }), 178 | // ]); 179 | 180 | // if (!nonce) { 181 | // const token = generateRandomString({ 182 | // length: 32, 183 | // lowercase: true, 184 | // numerical: true, 185 | // uppercase: true, 186 | // }); 187 | 188 | // await this.prismaService.nonce.create({ 189 | // data: { 190 | // nonce: token, 191 | // userId, 192 | // expiresAt: `${Date.now() + this.authService.TokenExpirationDuration}`, 193 | // }, 194 | // }); 195 | 196 | // return token; 197 | // } 198 | 199 | // return nonce.nonce; 200 | // } 201 | } 202 | -------------------------------------------------------------------------------- /src/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { AuthService } from './auth.service'; 8 | 9 | @Injectable() 10 | export class AuthGuard implements CanActivate { 11 | constructor(private authService: AuthService) {} 12 | 13 | async canActivate(context: ExecutionContext): Promise { 14 | const request = context.switchToHttp().getRequest(); 15 | const token = request.headers?.auth || request.cookies?.auth; 16 | 17 | if (!token) { 18 | throw new UnauthorizedException(); 19 | } 20 | 21 | const res = await this.authService.isTokenValid(token); 22 | 23 | if (!res || !res.user) { 24 | throw new UnauthorizedException('Invalid auth token'); 25 | } 26 | 27 | request.userId = res.user.id; 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { AuthService } from './auth.service'; 4 | import { AuthController } from './auth.controller'; 5 | import { UsersService } from 'src/user/users.service'; 6 | import { UsersModule } from 'src/user/users.module'; 7 | import { PrismaService } from 'src/prisma.service'; 8 | import { FlowModule } from 'src/flow/flow.module'; 9 | 10 | @Module({ 11 | imports: [UsersModule, forwardRef(() => FlowModule)], 12 | providers: [ 13 | AuthService, 14 | PrismaService, 15 | AuthController, 16 | ConfigService, 17 | UsersService, 18 | ], 19 | controllers: [AuthController], 20 | exports: [AuthService], 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { generateRandomString } from 'src/utils'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | import { SiweMessage } from 'siwe'; 5 | import { verifyMessage } from 'viem'; 6 | // import { chain, thirdwebClient } from 'src/thirdweb'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | private readonly logger = new Logger(AuthService.name); 11 | constructor(private readonly prismaService: PrismaService) {} 12 | 13 | /** 14 | * 48 hours 15 | */ 16 | public TokenExpirationDuration = 48 * 60 * 60 * 1000; 17 | 18 | /** 19 | * 2 minutes 20 | */ 21 | public NonceExpirationDuration = 2 * 60 * 1000; 22 | 23 | // getUserId = async (walletAddress: string) => { 24 | // const { id } = await this.prismaService.user.findFirst({ 25 | // select: { id: true }, 26 | // where: { address: walletAddress }, 27 | // }); 28 | 29 | // return id; 30 | // }; 31 | 32 | cleanUpExpiredNonces = async () => { 33 | await this.prismaService.nonce.deleteMany({ 34 | where: { 35 | expiresAt: { 36 | lt: `${Date.now()}`, 37 | }, 38 | }, 39 | }); 40 | }; 41 | 42 | // assignOtp = async (userId: number) => { 43 | // const [record, user] = await Promise.all([ 44 | // this.prismaService.otp.findFirst({ 45 | // where: { 46 | // userId, 47 | // expiresAt: { 48 | // gte: `${Date.now()}`, 49 | // }, 50 | // }, 51 | // include: { user: true }, 52 | // }), 53 | // this.prismaService.user.findUnique({ 54 | // where: { id: userId }, 55 | // select: { badges: true, identity: true }, 56 | // }), 57 | // ]); 58 | 59 | // if (!user) throw new InternalServerErrorException("User doesn't exist"); 60 | 61 | // if (user.identity?.valueOf() || user.badges?.valueOf()) 62 | // throw new ForbiddenException('User has already connected'); 63 | 64 | // if (record) return record.otp; 65 | 66 | // const otp = generateRandomString({ length: 6, numerical: true }); 67 | // await this.prismaService.otp.deleteMany({ 68 | // where: { 69 | // userId, 70 | // }, 71 | // }); 72 | // await this.prismaService.otp.create({ 73 | // data: { 74 | // otp, 75 | // userId, 76 | // expiresAt: `${Date.now() + 4 * 60 * 60 * 1000}`, // 4 hours 77 | // }, 78 | // }); 79 | 80 | // return otp; 81 | // }; 82 | 83 | // checkOtpValidity = async (otp: string) => { 84 | // const record = await this.prismaService.otp.findFirst({ 85 | // where: { 86 | // otp, 87 | // expiresAt: { 88 | // gte: `${Date.now()}`, 89 | // }, 90 | // }, 91 | // include: { user: true }, 92 | // }); 93 | 94 | // if (!record) return false; 95 | 96 | // return record.user.id; 97 | // }; 98 | 99 | generateNonce = () => { 100 | const nonce = generateRandomString({ 101 | length: 48, 102 | lowercase: true, 103 | numerical: true, 104 | uppercase: true, 105 | }); 106 | 107 | // Replace this with a cron job (or better with a key/value db) 108 | if (Math.random() < 0.2) setTimeout(() => this.cleanUpExpiredNonces(), 0); 109 | 110 | return nonce; 111 | }; 112 | 113 | isNonceValid = async (nonce: string) => { 114 | const isValid = await this.prismaService.nonce.findFirst({ 115 | where: { nonce }, 116 | }); 117 | 118 | if (isValid === null) throw new Error('Unavailable nonce'); 119 | if (isValid.expiresAt < `${Date.now()}`) throw new Error('Expired nonce'); 120 | 121 | return true; 122 | }; 123 | 124 | isTokenValid = async (token: string) => { 125 | const user = await this.prismaService.nonce.findFirst({ 126 | select: { user: true }, 127 | where: { 128 | nonce: token, 129 | userId: { 130 | not: null, 131 | }, 132 | expiresAt: { 133 | gt: `${Date.now()}`, 134 | }, 135 | }, 136 | }); 137 | 138 | if (user === null) return false; 139 | 140 | return user; 141 | }; 142 | 143 | verifyUser = async ( 144 | message: string, 145 | signature: `0x${string}`, 146 | address: `0x${string}`, 147 | ) => { 148 | try { 149 | // await this.isNonceValid(message.nonce); 150 | const valid = await verifyMessage({ 151 | address, 152 | message, 153 | signature, 154 | }); 155 | return valid; 156 | } catch (err) { 157 | return false; 158 | } 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /src/auth/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { SiweMessage } from 'siwe'; 3 | 4 | export class SiweMessageClass { 5 | @ApiProperty({ 6 | description: 'RFC 4501 dns authority that is requesting the signing.', 7 | }) 8 | domain: string; 9 | 10 | @ApiProperty({ 11 | description: `Ethereum address performing the signing conformant to capitalization 12 | encoded checksum specified in EIP-55 where applicable.`, 13 | }) 14 | address: string; 15 | 16 | @ApiProperty({ 17 | description: `Human-readable ASCII assertion that the user will sign`, 18 | required: false, 19 | }) 20 | statement?: string; 21 | 22 | @ApiProperty({ 23 | description: `RFC 3986 URI referring to the resource that is the subject of the signing 24 | (as in the __subject__ of a claim).`, 25 | }) 26 | uri: string; 27 | 28 | @ApiProperty({ description: `Current version of the message.` }) 29 | version: string; 30 | 31 | @ApiProperty({ 32 | description: `EIP-155 Chain ID to which the session is bound, and the network where 33 | Contract Accounts must be resolved.`, 34 | }) 35 | chainId: number; 36 | 37 | @ApiProperty({ 38 | description: `Randomized token used to prevent replay attacks, at least 8 alphanumeric 39 | characters.`, 40 | }) 41 | nonce: string; 42 | 43 | @ApiProperty({ 44 | description: `ISO 8601 datetime string of the current time.`, 45 | required: false, 46 | }) 47 | issuedAt?: string; 48 | 49 | @ApiProperty({ 50 | description: `ISO 8601 datetime string that, if present, indicates when the signed 51 | authentication message is no longer valid.`, 52 | required: false, 53 | }) 54 | expirationTime?: string; 55 | 56 | @ApiProperty({ 57 | description: `ISO 8601 datetime string that, if present, indicates when the signed 58 | * authentication message will become valid.`, 59 | required: false, 60 | }) 61 | notBefore?: string; 62 | 63 | @ApiProperty({ 64 | description: `System-specific identifier that may be used to uniquely refer to the 65 | * sign-in request.`, 66 | required: false, 67 | }) 68 | requestId?: string; 69 | 70 | @ApiProperty({ 71 | description: `List of information or references to information the user wishes to have 72 | resolved as part of authentication by the relying party. They are 73 | expressed as RFC 3986 URIs separated by`, 74 | required: false, 75 | }) 76 | resources?: Array; 77 | } 78 | 79 | export class LoginDTO { 80 | @ApiProperty() 81 | message: string; 82 | 83 | @ApiProperty() 84 | signature: string; 85 | 86 | @ApiProperty() 87 | address: string; 88 | } 89 | -------------------------------------------------------------------------------- /src/auth/dto/otp.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNumberString, Length } from 'class-validator'; 3 | 4 | export class OtpDTO { 5 | @ApiProperty() 6 | @IsNumberString() 7 | @Length(6, 6) 8 | otp: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/collection/colleciton.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CollectionService } from './collection.service'; 3 | import { CollectionController } from './collection.controller'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | import { AuthModule } from 'src/auth/auth.module'; 6 | import { FlowModule } from 'src/flow/flow.module'; 7 | 8 | @Module({ 9 | imports: [AuthModule, FlowModule], 10 | providers: [CollectionService, PrismaService, CollectionController], 11 | controllers: [CollectionController], 12 | exports: [CollectionService], 13 | }) 14 | export class CollectionModule {} 15 | -------------------------------------------------------------------------------- /src/collection/collection.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger, Param, Req, UseGuards } from '@nestjs/common'; 2 | 3 | import { CollectionService } from './collection.service'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | import { ApiQuery } from '@nestjs/swagger'; 6 | import { AuthGuard } from 'src/auth/auth.guard'; 7 | import { AuthedReq } from 'src/utils/types/AuthedReq.type'; 8 | 9 | @Controller({ path: 'collection' }) 10 | export class CollectionController { 11 | private readonly logger = new Logger(CollectionController.name); 12 | constructor( 13 | private readonly collectionService: CollectionService, 14 | private readonly prismaService: PrismaService, 15 | ) {} 16 | 17 | @UseGuards(AuthGuard) 18 | @ApiQuery({ 19 | name: 'cid', 20 | description: 'id of the collection', 21 | required: false, 22 | }) 23 | @Get(':id') 24 | async getCollections(@Param('id') id: number, @Req() { userId }: AuthedReq) { 25 | const collection = await this.collectionService.getCollection(id, userId); 26 | return collection; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/collection/collection.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, Logger } from '@nestjs/common'; 2 | import { ProjectType } from '@prisma/client'; 3 | import { FlowService } from 'src/flow/flow.service'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | 6 | @Injectable() 7 | export class CollectionService { 8 | private readonly logger = new Logger(CollectionService.name); 9 | constructor( 10 | private readonly prismaService: PrismaService, 11 | private readonly flowService: FlowService, 12 | ) {} 13 | 14 | getCollection = async (id: number, userId: number) => { 15 | const collection = await this.prismaService.project.findUnique({ 16 | where: { id, type: { not: ProjectType.project } }, 17 | }); 18 | 19 | if (!collection) throw new BadRequestException('Invalid id'); 20 | 21 | return { 22 | collection, 23 | progress: await this.flowService.getCollectionProgressStatus(userId, id), 24 | // started: await this.flowService.isCollectionStarted(userId, id), 25 | }; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/flow/dto/bodies.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsDefined, IsIn } from 'class-validator'; 3 | 4 | export class SetCoIDto { 5 | @ApiProperty({ description: 'project id' }) 6 | @IsDefined() 7 | pid: number; 8 | } 9 | 10 | export class DnDBody { 11 | @ApiProperty({ description: 'collection id' }) 12 | @IsDefined() 13 | collectionId: number; 14 | 15 | @ApiProperty({ description: 'New order of the projects in descending order' }) 16 | @IsDefined() 17 | projectIds: number[]; 18 | } 19 | 20 | export class RemoveLastVoteDto { 21 | @ApiProperty({ 22 | type: 'number', 23 | description: 24 | 'Collection id (null for the top-level collection comparisons)', 25 | }) 26 | @IsDefined() 27 | collectionId: number | null; 28 | } 29 | -------------------------------------------------------------------------------- /src/flow/dto/editedRanking.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsInt, IsJSON, IsOptional, Validate } from 'class-validator'; 3 | import { IsNumberOrNull } from './voteProjects.dto'; 4 | 5 | export class EditedRankingDto { 6 | @Validate(IsNumberOrNull) 7 | @ApiProperty() 8 | collectionId: number | null; 9 | 10 | @IsJSON() 11 | @ApiProperty() 12 | ranking: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/flow/dto/pairsResult.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class PairsResult { 4 | @ApiProperty({ 5 | type: 'array', 6 | items: { 7 | type: 'object', 8 | }, 9 | }) 10 | pairs: unknown[]; 11 | 12 | @ApiProperty() 13 | totalPairs: number; 14 | 15 | @ApiProperty() 16 | votedPairs: number; 17 | 18 | @ApiProperty() 19 | name: string; 20 | } 21 | 22 | export class ExpertisePairs extends PairsResult {} 23 | -------------------------------------------------------------------------------- /src/flow/dto/share.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNumber, Max, Min } from 'class-validator'; 3 | 4 | export class ShareDto { 5 | @IsNumber() 6 | @ApiProperty() 7 | id: number; 8 | 9 | @Max(1) 10 | @Min(0) 11 | @IsNumber() 12 | @ApiProperty() 13 | share: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/flow/dto/voteCollections.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsInt, Validate } from 'class-validator'; 3 | import { IsNumberOrNull } from './voteProjects.dto'; 4 | 5 | export class VoteCollectionsDTO { 6 | @IsInt() 7 | @ApiProperty() 8 | collection1Id: number; 9 | 10 | @IsInt() 11 | @ApiProperty() 12 | collection2Id: number; 13 | 14 | @Validate(IsNumberOrNull) 15 | @ApiProperty() 16 | pickedId: number | null; 17 | } 18 | -------------------------------------------------------------------------------- /src/flow/dto/voteProjects.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsInt, 4 | Max, 5 | Min, 6 | Validate, 7 | ValidationArguments, 8 | ValidatorConstraint, 9 | ValidatorConstraintInterface, 10 | } from 'class-validator'; 11 | 12 | @ValidatorConstraint() 13 | export class IsNumberOrNull implements ValidatorConstraintInterface { 14 | validate(value: any) { 15 | return ( 16 | (typeof value === 'number' && Number.isInteger(value)) || value === null 17 | ); 18 | } 19 | 20 | defaultMessage(args: ValidationArguments) { 21 | return `Property ${args.property} must be a number or null`; 22 | } 23 | } 24 | 25 | @ValidatorConstraint() 26 | export class IsRating implements ValidatorConstraintInterface { 27 | validate(value: any) { 28 | if (typeof value === 'number') { 29 | if (value >= 1 && value <= 5 && Number.isInteger(value)) return true; 30 | return false; 31 | } 32 | return value === null; 33 | } 34 | 35 | defaultMessage(args: ValidationArguments) { 36 | return `Property ${args.property} must be an integer (1 <= r <= 5) or null`; 37 | } 38 | } 39 | 40 | export type Rating = 1 | 2 | 3 | 4 | 5 | null; 41 | 42 | export class VoteProjectsDTO { 43 | @IsInt() 44 | @ApiProperty() 45 | project1Id: number; 46 | 47 | @IsInt() 48 | @ApiProperty() 49 | project2Id: number; 50 | 51 | @Validate(IsNumberOrNull) 52 | @ApiProperty() 53 | pickedId: number | null; 54 | 55 | @Validate(IsRating) 56 | @ApiProperty() 57 | project1Stars: Rating; 58 | 59 | @Validate(IsRating) 60 | @ApiProperty() 61 | project2Stars: Rating; 62 | } 63 | -------------------------------------------------------------------------------- /src/flow/flow.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { FlowService } from './flow.service'; 3 | import { FlowController } from './flow.controller'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | import { AuthModule } from 'src/auth/auth.module'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => AuthModule)], 9 | providers: [FlowService, PrismaService, FlowController], 10 | controllers: [FlowController], 11 | exports: [FlowService], 12 | }) 13 | export class FlowModule {} 14 | -------------------------------------------------------------------------------- /src/flow/types/index.ts: -------------------------------------------------------------------------------- 1 | import { GetResult } from '@prisma/client/runtime/library'; 2 | 3 | export interface CollectionRanking { 4 | type: 'collection' | 'composite project'; 5 | hasRanking: true; 6 | // isFinished: boolean; 7 | id: number; 8 | RPGF4Id: string; 9 | name: string; 10 | rank: number; 11 | ranking: (CollectionRanking | ProjectRanking)[]; 12 | } 13 | 14 | export interface ProjectRanking { 15 | type: 'project' | 'collection' | 'composite project'; 16 | hasRanking: false; 17 | id: number; 18 | RPGF4Id: string; 19 | rank: number; 20 | name: string; 21 | } 22 | 23 | export interface EditingCollectionRanking extends CollectionRanking { 24 | locked: boolean; 25 | error: boolean; 26 | expanded: boolean; 27 | ranking: (EditingCollectionRanking | EditingProjectRanking)[]; 28 | } 29 | 30 | export interface EditingProjectRanking extends ProjectRanking { 31 | locked: boolean; 32 | error: boolean; 33 | } 34 | 35 | export type CollectionProgressStatus = 36 | // | 'Attested' 37 | | 'Finished' 38 | | 'WIP - Threshold' 39 | | 'WIP' 40 | // | 'Filtered' 41 | // | 'Filtering' 42 | | 'Pending'; 43 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | import * as fs from 'fs'; 5 | import * as cors from 'cors'; 6 | import * as cookieParser from 'cookie-parser'; 7 | import { ValidationPipe } from '@nestjs/common'; 8 | import { json, urlencoded } from 'express'; 9 | // import { main } from './read-rpgf4-projects'; 10 | // import { main } from './project-reading'; 11 | 12 | const CorsWhitelist = [ 13 | 'https://rpgf5-prototype.vercel.app/', 14 | 'https://rpgf5-prototype.vercel.app', 15 | 'https://localhost:3001', 16 | 'http://localhost:3000', 17 | 'https://staging.pairwise.generalmagic.io', 18 | 'https://staging.pairwise.vote/', 19 | 'https://staging.pairwise.vote', 20 | 'https://www.pairwise.vote', 21 | 'https://pairwise.vote', 22 | 'https://pairwise.vote/', 23 | 'https://www.pairwise.vote/', 24 | 'http://www.pairwise.vote', 25 | 'http://pairwise.vote', 26 | 'https://staging.app.pairwise.vote/', 27 | 'https://app.pairwise.vote/', 28 | 'https://staging.app.pairwise.vote', 29 | 'https://app.pairwise.vote', 30 | 'https://www.staging.app.pairwise.vote', 31 | 'https://www.app.pairwise.vote', 32 | 'https://www.staging.app.pairwise.vote/', 33 | 'https://www.app.pairwise.vote/', 34 | 'http://pairwise.vote/', 35 | 'http://www.pairwise.vote/', 36 | 'https://pairwise-frontend-git-test-numerous-planets-general-magic.vercel.app', 37 | 'https://pwrd.cupofjoy.store', 38 | 'pairwise.vote', 39 | ]; 40 | 41 | async function bootstrap() { 42 | // main(); 43 | // return; 44 | let httpsOptions = undefined; 45 | if (process.env.NODE_ENV === 'development') { 46 | httpsOptions = { 47 | key: fs.readFileSync('./certs/cert.key'), 48 | cert: fs.readFileSync('./certs/cert.crt'), 49 | }; 50 | } 51 | const app = await NestFactory.create(AppModule, { httpsOptions }); 52 | app.use(json({ limit: '5mb' })); 53 | app.use(urlencoded({ extended: true, limit: '5mb' })); 54 | // app.enableCors(); 55 | // app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); 56 | app.useGlobalPipes(new ValidationPipe({ transform: true })); 57 | // app.use(cors()); 58 | 59 | // app.use((req: any, res: any) => { 60 | // res.header('Access-Control-Allow-Origin', '*'); 61 | // res.header( 62 | // 'Access-Control-Allow-Headers', 63 | // 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With, Auth', 64 | // ); 65 | // res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); 66 | // //... 67 | // }); 68 | 69 | // const corsOptions = { 70 | // origin(origin: any, callback: any) { 71 | // return callback(null, true); 72 | // }, 73 | // }; 74 | 75 | app.use( 76 | cors({ 77 | credentials: true, 78 | // allowedHeaders: ['Auth'], 79 | origin: (origin, callback) => { 80 | if ( 81 | !origin || 82 | CorsWhitelist.filter( 83 | (item) => origin.includes(item) || item.includes(origin), 84 | ).length > 0 || 85 | (origin.includes('vercel.app') && origin.includes('pairwise')) || 86 | origin.includes('localhost:') 87 | ) 88 | return callback(null, true); 89 | return callback(new Error('Not allowed by CORS')); 90 | }, 91 | }), 92 | ); 93 | 94 | // app.use(function (req: any, res: any, next: any) { 95 | // res.setHeader( 96 | // 'Access-Control-Allow-Headers', 97 | // 'X-Requested-With,content-type,auth', 98 | // ); 99 | // next(); 100 | // }); 101 | 102 | // app.use(function (req: any, res: any, next: any) { 103 | // res.setHeader('Access-Control-Allow-Origin', '*'); 104 | // res.setHeader( 105 | // 'Access-Control-Allow-Methods', 106 | // 'GET, POST, OPTIONS, PUT, PATCH, DELETE', 107 | // ); 108 | // res.setHeader( 109 | // 'Access-Control-Allow-Headers', 110 | // 'X-Requested-With,content-type', 111 | // ); 112 | // res.setHeader('Access-Control-Allow-Credentials', true); 113 | // next(); 114 | // }); 115 | 116 | // app.all('*', function (req, res) { 117 | // res.header('Access-Control-Allow-Origin', '*'); 118 | // res.header( 119 | // 'Access-Control-Allow-Headers', 120 | // 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With', 121 | // ); 122 | // res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); 123 | // //... 124 | // }); 125 | 126 | app.use(cookieParser()); 127 | 128 | const config = new DocumentBuilder() 129 | .setTitle('Pairwise') 130 | .setDescription('Pairwise API Application') 131 | .setVersion('v0.1') 132 | .build(); 133 | 134 | const document = SwaggerModule.createDocument(app, config); 135 | SwaggerModule.setup('swagger', app, document); 136 | 137 | await app.listen(process.env.PORT || 7070, '0.0.0.0'); 138 | } 139 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 140 | bootstrap(); 141 | -------------------------------------------------------------------------------- /src/mock/mock.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger } from '@nestjs/common'; 2 | import { ProjectType } from '@prisma/client'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | 5 | @Controller({ path: 'mock' }) 6 | export class MockController { 7 | private readonly logger = new Logger(MockController.name); 8 | constructor(private readonly prismaService: PrismaService) {} 9 | 10 | @Get('/collections') 11 | async getCollections() { 12 | const collections = await this.prismaService.project.findMany({ 13 | where: { type: ProjectType.collection }, 14 | }); 15 | 16 | return collections; 17 | } 18 | 19 | @Get('/projects') 20 | async getProjects() { 21 | const projects = await this.prismaService.project.findMany(); 22 | 23 | return projects; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mock/mock.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma.service'; 3 | import { MockController } from './mock.controller'; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [PrismaService], 8 | exports: [], 9 | controllers: [MockController], 10 | }) 11 | export class MocksModule {} 12 | -------------------------------------------------------------------------------- /src/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService extends PrismaClient implements OnModuleInit { 6 | async onModuleInit() { 7 | await this.$connect(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/project-reading/index.ts: -------------------------------------------------------------------------------- 1 | import { parse as json2csv } from 'json2csv'; 2 | import * as fs from 'fs'; 3 | import axios from 'axios'; 4 | 5 | interface Profile { 6 | bannerImageUrl: string; 7 | profileImageUrl: string; 8 | id: string; 9 | } 10 | 11 | interface ContributionLink { 12 | description: string; 13 | type: string; 14 | url: string; 15 | } 16 | 17 | interface FundingSource { 18 | type: string; 19 | currency: string; 20 | amount: string; 21 | description: string; 22 | } 23 | 24 | interface JsonObject { 25 | id: string; 26 | displayName: string; 27 | contributionDescription: string; 28 | impactDescription: string; 29 | bio: string; 30 | websiteUrl: string; 31 | applicantType: string; 32 | impactCategory: string[]; 33 | prelimResult: string; 34 | reportReason: string; 35 | includedInBallots: number; 36 | lists: string[]; 37 | contributionLinks: ContributionLink[]; 38 | fundingSources: FundingSource[]; 39 | impactMetrics: string[]; 40 | github: string[]; 41 | packages: string[]; 42 | profile: Profile; 43 | } 44 | 45 | // const jsonArray: JsonObject[] = [ 46 | // { 47 | // id: '0xeb7d879d09a9077dc3cf8cdbee26e17bcc00f19750a7c6f137ab9a3ef703139a', 48 | // displayName: '👑 King of the Degens 🎩 - Farcaster Frame Game', 49 | // contributionDescription: 50 | // "I'm Corbin Page (https://warpcast.com/corbin.eth) and my team and I look for fun new ways to onboard folks into web3 and the Optimism ecosystem. This quarter we built a novel crypto game using Farcaster Frames and streaming $DEGEN tokens. We had over 870 players across two Seasons and more than 1.5M DEGEN distributed to them. Notable our churn was really low with over half the players playing over 5 times despite the .001 ETH entry price on Base. \n\nKing of the Degens is an Onchain RPG with real crypto stakes built on Farcaster. Up in the castle, the King and 9 court members are being streamed $DEGEN points in real-time. A player can **⚔️ STORM THE CASTLE ⚔️** to join the court by paying 0.001 ETH, which is swapped for $DEGEN and put in the Treasury for all to share.", 51 | // impactDescription: '', 52 | // bio: "I'm Corbin Page (https://warpcast.com/corbin.eth) and my team and I look for fun new ways to onboard folks into web3 and the Optimism ecosystem. This quarter we built a novel crypto game using Farcaster Frames and streaming $DEGEN tokens. We had over 870 players across two Seasons and more than 1.5M DEGEN distributed to them. Notable our churn was really low with over half the players playing over 5 times despite the .001 ETH entry price on Base. \n\nKing of the Degens is an Onchain RPG with real crypto stakes built on Farcaster. Up in the castle, the King and 9 court members are being streamed $DEGEN points in real-time. A player can **⚔️ STORM THE CASTLE ⚔️** to join the court by paying 0.001 ETH, which is swapped for $DEGEN and put in the Treasury for all to share.", 53 | // websiteUrl: 'degen.game/kotd', 54 | // applicantType: 'PROJECT', 55 | // impactCategory: ['Social'], 56 | // prelimResult: 'Keep', 57 | // reportReason: '', 58 | // includedInBallots: 0, 59 | // lists: [], 60 | // contributionLinks: [ 61 | // { 62 | // description: '0x12BE8ef11d78a09bE19Fe8680cdA0538Aef87E9c', 63 | // type: '8453', 64 | // url: 'https://basescan.org/address/0x12BE8ef11d78a09bE19Fe8680cdA0538Aef87E9c', 65 | // }, 66 | // { 67 | // description: '0x40Ec213312B4BFE20BAA68f7a3899115350A6607', 68 | // type: '8453', 69 | // url: 'https://basescan.org/address/0x40Ec213312B4BFE20BAA68f7a3899115350A6607', 70 | // }, 71 | // ], 72 | // fundingSources: [ 73 | // { 74 | // type: 'Revenue', 75 | // currency: 'USD', 76 | // amount: 'Under 250k', 77 | // description: 78 | // "There's a small fee on each game transaction. This revenue will likely go into a protocol DAO.", 79 | // }, 80 | // ], 81 | // impactMetrics: [], 82 | // github: ['https://github.com/corbinpage/kotd-contracts-public'], 83 | // packages: [], 84 | // profile: { 85 | // bannerImageUrl: 86 | // 'https://storage.googleapis.com/op-atlas/6c77ce81-7908-437b-98e3-06b971a2ed5b.png', 87 | // profileImageUrl: 88 | // 'https://storage.googleapis.com/op-atlas/d275ec51-6e3a-416b-aee5-1cdc16c4c6c2.png', 89 | // id: '0xeb7d879d09a9077dc3cf8cdbee26e17bcc00f19750a7c6f137ab9a3ef703139a', 90 | // }, 91 | // }, 92 | // ]; 93 | 94 | // Filter out object or array fields 95 | const filterJsonArray = (jsonArray: FilteredJsonObject[]) => 96 | jsonArray.map((item) => { 97 | const filteredItem: { [key: string]: any } = {}; 98 | let key: keyof FilteredJsonObject; 99 | for (key in item) { 100 | if (typeof item[key] === 'string' || typeof item[key] === 'number') { 101 | filteredItem[key] = item[key]; 102 | } 103 | } 104 | return filteredItem; 105 | }); 106 | 107 | interface FilteredJsonObject extends Omit { 108 | initialCategory: string; 109 | recategorization: string; 110 | details: string; 111 | } 112 | 113 | export const main = async () => { 114 | const baseUrl = `https://round4-api-eas.retrolist.app/projects`; 115 | const { data: jsonObjects } = await axios.get(baseUrl); 116 | let count = 0; 117 | console.log('Fetching all projects done'); 118 | 119 | const totalProjects: FilteredJsonObject[] = []; 120 | for (const project of jsonObjects) { 121 | const { data: projectDetails } = await axios.get( 122 | `${baseUrl}/${project.id}`, 123 | ); 124 | 125 | totalProjects.push({ 126 | ...projectDetails, 127 | initialCategory: project.impactCategory[0], 128 | recategorization: project.impactCategory[0], 129 | details: `${baseUrl}/${project.id}`, 130 | }); 131 | 132 | console.log('Fetching', count++, 'project details done'); 133 | // if (count > 20) break; 134 | } 135 | 136 | const filteredArray = filterJsonArray( 137 | totalProjects.sort((a, b) => 138 | a.initialCategory.localeCompare(b.initialCategory), 139 | ), 140 | ); 141 | // Convert JSON to CSV 142 | const csv = json2csv(filteredArray); 143 | 144 | // Write CSV to a file 145 | fs.writeFileSync('output.csv', csv); 146 | console.log('CSV file has been written successfully.'); 147 | }; 148 | -------------------------------------------------------------------------------- /src/project/project.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Logger, Param, UseGuards } from '@nestjs/common'; 2 | import { ProjectService } from './project.service'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | import { ApiQuery } from '@nestjs/swagger'; 5 | import { AuthGuard } from 'src/auth/auth.guard'; 6 | 7 | @Controller({ path: 'project' }) 8 | export class ProjectController { 9 | private readonly logger = new Logger(ProjectController.name); 10 | constructor( 11 | private readonly projectService: ProjectService, 12 | private readonly prismaService: PrismaService, 13 | ) {} 14 | 15 | @UseGuards(AuthGuard) 16 | @ApiQuery({ 17 | name: 'cid', 18 | description: 'id of the project', 19 | required: false, 20 | }) 21 | @Get(':id') 22 | async getProjects(@Param('id') id: number) { 23 | const project = await this.projectService.getProject(id); 24 | return project; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/project/project.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProjectService } from './project.service'; 3 | import { ProjectController } from './project.controller'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | import { AuthModule } from 'src/auth/auth.module'; 6 | import { FlowModule } from 'src/flow/flow.module'; 7 | 8 | @Module({ 9 | imports: [AuthModule, FlowModule], 10 | providers: [ProjectService, PrismaService, ProjectController], 11 | controllers: [ProjectController], 12 | exports: [ProjectService], 13 | }) 14 | export class ProjectModule {} 15 | -------------------------------------------------------------------------------- /src/project/project.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, Logger } from '@nestjs/common'; 2 | import { ProjectType } from '@prisma/client'; 3 | import { FlowService } from 'src/flow/flow.service'; 4 | import { PrismaService } from 'src/prisma.service'; 5 | 6 | @Injectable() 7 | export class ProjectService { 8 | private readonly logger = new Logger(ProjectService.name); 9 | constructor(private readonly prismaService: PrismaService) {} 10 | 11 | getProject = async (id: number) => { 12 | const project = await this.prismaService.project.findUnique({ 13 | where: { id, type: ProjectType.project }, 14 | }); 15 | 16 | if (!project) throw new BadRequestException('Invalid id'); 17 | 18 | return { 19 | project, 20 | // progress: await this.flowService.getProjectProgressStatus(userId, id), 21 | // started: await this.flowService.isProjectStarted(userId, id), 22 | }; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/badgeholders.ts: -------------------------------------------------------------------------------- 1 | export const badgeholders = [ 2 | '0x5c30f1273158318d3dc8ffcf991421f69fd3b77d', 3 | '0x64af04a2fc6407a00951ad1ab534f4e0b23f3a56', 4 | '0xfdfab69f684876493fb4403be0da0acb8c64a88e', 5 | '0x0e6425c5666dd516993e99cafed9767830cb37f9', 6 | '0x5f4bcccb5c2cbb01c619f5cfed555466e31679b6', 7 | '0xce8925db997c654ca6830b645f7a7acfef2cc503', 8 | '0x56edf679b0c80d528e17c5ffe514dc9a1b254b9c', 9 | '0x2a99ec82d658f7a77ddebfd83d0f8f591769cb64', 10 | '0xf32dd1bd55bd14d929218499a2e7d106f72f79c7', 11 | '0xf8e30c251aa7974aa0a2d9e452d863681f8b59ac', 12 | '0xabf5f73c4e42672b00ccd4f3e692fd695781f0a4', 13 | '0xa3b5c58cef73ada157ceaecdeaa68d7ea2fc0592', 14 | '0x120dce11bc7bc745544129c0c53fb08e4f113ad2', 15 | '0xc9d5dc70a2cebce15f685148bbc7d53704e53b7f', 16 | '0x277d26a45add5775f21256159f089769892cea5b', 17 | '0xcafd432b7ecafff352d92fcb81c60380d437e99d', 18 | '0xb21c33de1fab3fa15499c62b59fe0cc3250020d1', 19 | '0xaef766ce8047a11cbb0f8264dea7559fd0b48444', 20 | '0x961aa96febee5465149a0787b03bfa14d8e9033f', 21 | '0xfa244692f6aa32c7eb01e46c4e8edb048348fe11', 22 | '0x68108902de3a5031197a6eb3b74b3b033e8e8e4d', 23 | '0xf5f0a5135486ff2715b1dfaead54eeaffe6b8404', 24 | '0x19f8a76474ebf9effb269ec5c2b935a3611d6779', 25 | '0x60ec4fd8069513f738f3a0f41b9e00c294e74bf3', 26 | '0xc0017405e287476443ab1b342b86f2ea92ef9f73', 27 | '0x8289432acd5eb0214b1c2526a5edb480aa06a9ab', 28 | '0x05ff834dd5a7edb437b061cb00108200bf4873d6', 29 | '0xf346100e892553dceb41a927fb668da7b0b7c964', 30 | '0x07bf3cda34aa78d92949bbdce31520714ab5b228', 31 | '0x07fda67513ec0897866098a11dc3858089d4a505', 32 | '0xcf79c7eaec5bdc1a9e32d099c5d6bdf67e4cf6e8', 33 | '0x3b60e31cfc48a9074cd5bebb26c9eaa77650a43f', 34 | '0x69e271483c38ed4902a55c3ea8aab9e7cc8617e5', 35 | '0x64fed9e56b548343e7bb47c49ecd7ffa9f1a34fe', 36 | '0x3db5b38ef4b433d9c6a664bd35551be73313189a', 37 | '0x66946def4ba6153c500d743b7a5febfc1654d6bd', 38 | '0xe422d6c46a69e989ba6468ccd0435cb0c5c243e3', 39 | '0xa50d0e425461ccab26ef30104cdd879b90db3843', 40 | '0x5a5d9ab7b1bd978f80909503ebb828879daca9c3', 41 | '0x6dc43be93a8b5fd37dc16f24872babc6da5e5e3e', 42 | '0xef32eb37f3e8b4bdddf99879b23015f309ed7304', 43 | '0xf68d2bfcecd7895bba05a7451dd09a1749026454', 44 | '0x06f455e2c297a4ae015191fa7a4a11c77c5b1b7c', 45 | '0x75536cf4f01c2bfa528f5c74ddc1232db3af3ee5', 46 | '0x92bf20563e747b2f8711549be17a9d7b876c4053', 47 | '0xac469c5df1ce6983ff925d00d1866ab780d402a4', 48 | '0x826976d7c600d45fb8287ca1d7c76fc8eb732030', 49 | '0x9c949881084dcbd97237f786710ab8e52a457136', 50 | '0x894aa5f1e45454677a8560dde3b45cb5c427ef92', 51 | '0xbf430a49c4d85aeed3908619d5387a1fbf8e74a9', 52 | '0x73186b2a81952c2340c4eb2e74e89869e1183df0', 53 | '0x7fc80fad32ec41fd5cfcc14eee9c31953b6b4a8b', 54 | '0x5d36a202687fd6bd0f670545334bf0b4827cc1e2', 55 | '0x55aed0ce035883626e536254dda2f23a5b5d977f', 56 | '0x15c6ac4cf1b5e49c44332fb0a1043ccab19db80a', 57 | '0x66da63b03feca7dd44a5bb023bb3645d3252fa32', 58 | '0x53e0b897eae600b2f6855fce4a42482e9229d2c2', 59 | '0xdb150346155675dd0c93efd960d5985244a34820', 60 | '0xdb5781a835b60110298ff7205d8ef9678ff1f800', 61 | '0x839395e20bbb182fa440d08f850e6c7a8f6f0780', 62 | '0x585639fbf797c1258eba8875c080eb63c833d252', 63 | '0x396a34c10b11e33a4bf6f3e6a419a23c54ad34fb', 64 | '0x534631bcf33bdb069fb20a93d2fdb9e4d4dd42cf', 65 | '0x1f5d295778796a8b9f29600a585ab73d452acb1c', 66 | '0xabf28f8d9adfb2255f4a059e37d3bce9104969db', 67 | '0x1de2a056508e0d0dd88a88f1f5cdf9cfa510795c', 68 | '0xf4b0556b9b6f53e00a1fdd2b0478ce841991d8fa', 69 | '0x288c53a1ba857ead34ad0e79f644087f8174185a', 70 | '0x12681667bb220521c222f50ece5eb752046bc144', 71 | '0x8f07bc36ff569312fdc41f3867d80bbd2fe94b76', 72 | '0x94db037207f6fb697dbd33524aadffd108819dc8', 73 | '0x00409fc839a2ec2e6d12305423d37cd011279c09', 74 | '0x1d3bf13f8f7a83390d03db5e23a950778e1d1309', 75 | '0x69dc230b06a15796e3f42baf706e0e55d4d5eaa1', 76 | '0x616cad18642f45d3fa5fcaad0a2d81764a9cba84', 77 | '0xd35e119782059a27fead4edda8b555f393650bc8', 78 | '0x9934465ee73beaf148b1b3ff232c8cd86c4c2c63', 79 | '0xc2e2b715d9e302947ec7e312fd2384b5a1296099', 80 | '0xdc0a92c350a52b6583e235a57901b8731af8b249', 81 | '0x665d84fffddd72d24df555e6b065b833478dffca', 82 | '0xdcf7be2ff93e1a7671724598b1526f3a33b1ec25', 83 | '0x5e349eca2dc61abcd9dd99ce94d04136151a09ee', 84 | '0xa142ab9eab9264807a41f0e5cbdab877d204e233', 85 | '0xac3a69dd4a8fecc18b172bfa9643d6b0863819c8', 86 | '0x399e0ae23663f27181ebb4e66ec504b3aab25541', 87 | '0x5554672e67ba866b9861701d0e0494ab324ad19a', 88 | '0x29c4dbc1a81d06c9aa2faed93bb8b4a78f3eabdb', 89 | '0x53c61cfb8128ad59244e8c1d26109252ace23d14', 90 | '0x91031dcfdea024b4d51e775486111d2b2a715871', 91 | '0x849151d7d0bf1f34b70d5cad5149d28cc2308bf1', 92 | '0x34aa3f359a9d614239015126635ce7732c18fdf3', 93 | '0xa3eac0016f6581ac34768c0d4b99ddcd88071c3c', 94 | '0x490c91f38ec57e3ab00811e0c51a62bfed7e81f4', 95 | '0xd31b671f1a398b519222fdaba5ab5464b9f2a3fa', 96 | '0x801707059a55d748b23b02043c71b7a3d976f071', 97 | '0x17640d0d8c93bf710b6ee4208997bb727b5b7bc2', 98 | '0x1e6d9f536a5d1cc04fc13b3133efdb90c8ee5ea1', 99 | '0x7899d9b1181cbb427b0b1be0684c096c260f7474', 100 | '0x434f5325ddcdbbfcce64be2617c72c4aa33ec3e7', 101 | '0x0331969e189d63fbc31d771bb04ab44227d748d8', 102 | '0x9194efdf03174a804f3552f4f7b7a4bb74badb7f', 103 | '0xeee718c1e522ecb4b609265db7a83ab48ea0b06f', 104 | '0x23936429fc179da0e1300644fb3b489c736d562f', 105 | '0xe53e89d978ff1da716f80baa6e6d8b3fa23f2284', 106 | '0x5555763613a12d8f3e73be831dff8598089d3dca', 107 | '0x75cac0ceb8a39ddb4942a83ad2aafaf0c2a3e13f', 108 | '0x14276eb29e90541831cb94c80331484ae6d2a1d8', 109 | '0xaeb99a255c3a243ab3e4f654041e9bf5340cf313', 110 | '0xdadd7c883288cfe2e257b0a361865e5e9349808b', 111 | '0x60ca282757ba67f3adbf21f3ba2ebe4ab3eb01fc', 112 | '0x378c23b326504df4d29c81ba6757f53b2c59f315', 113 | '0xdd7a79b1b6e8dd444f99d68a7d493a85556944a2', 114 | '0xde2b6860cb3212a6a1f8f8628abfe076723a4b39', 115 | '0x57893e666bd15e886d74751b0879361a3383b57a', 116 | '0x5872ce037211233b9f6f5095c25988021f270c21', 117 | '0xdcf09a83e9cc4611b2215bfb7116bfaf5e906d3d', 118 | '0x6eda5acaff7f5964e1ecc3fd61c62570c186ca0c', 119 | '0x48a63097e1ac123b1f5a8bbffafa4afa8192fab0', 120 | '0xb0623c91c65621df716ab8afe5f66656b21a9108', 121 | '0x925afeb19355e289ed1346ede709633ca8788b25', 122 | '0x146cfed833cc926b16b0da9257e8a281c2add9f3', 123 | '0x28f569cc6c29d804a1720edc16bf1ebab2ea35b4', 124 | '0x45a10f35befa4ab841c77860204b133118b7ccae', 125 | '0x8eb9e5e5375b72ee7c5cb786ce8564d854c26a86', 126 | '0x308fedfb88f6e85f27b85c8011ccb9b5e15bcbf7', 127 | ]; 128 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/ballot.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "project_id": "0x04d4b89e72e276bebc4bb4359145bc89e12f64a1edb1d85e97a5aa9c65660da5", 4 | "allocation": "2.662", 5 | "impact": 3 6 | }, 7 | { 8 | "project_id": "0x1547a722f7731b439b97428c31925100ef49ae443e32f5bba766268c296a3379", 9 | "allocation": "4.474", 10 | "impact": 2 11 | }, 12 | { 13 | "project_id": "0x29689510e5add50d929566fcbb78a8f85fac28545928859f97ce6c5ca3c97da1", 14 | "allocation": "5.123", 15 | "impact": 3 16 | }, 17 | { 18 | "project_id": "0x2b5b44d0e98599b5c877792367dcd0ed8f8f849fb267812a936897aa17baa6c4", 19 | "allocation": "0.697", 20 | "impact": 2 21 | }, 22 | { 23 | "project_id": "0x3142a5d468f9c6cd587619a0fccf73dc77835149f1ca31e579cb51501dcfd285", 24 | "allocation": "2.575", 25 | "impact": 3 26 | }, 27 | { 28 | "project_id": "0x37df31043d401a09c24ba1066e602ff34c2906ac92397040b5b90694d74eb8d7", 29 | "allocation": "3.592", 30 | "impact": 3 31 | }, 32 | { 33 | "project_id": "0x3d99c996bf2979b7ff827a4b2b2c56c65580bc75854ac41b30f034465801f301", 34 | "allocation": "1.656", 35 | "impact": 2 36 | }, 37 | { 38 | "project_id": "0x3fe005facaaa824b2fd2190be9a1fa124577580dbcdf91f85308725161a79990", 39 | "allocation": "3.521", 40 | "impact": 1 41 | }, 42 | { 43 | "project_id": "0x41275977def21cc80eaddabfc98c04cf02df9c92c918070437d9619a8151b9f5", 44 | "allocation": "2.256", 45 | "impact": 4 46 | }, 47 | { 48 | "project_id": "0x417e4e0fab598eee0958d618dfa22dc4ad60eaaa659b811c4780da1df7c7aad6", 49 | "allocation": "0.162", 50 | "impact": 3 51 | }, 52 | { 53 | "project_id": "0x41e9bd84d87b85a4190e4ce47865b14733f12e803cf97c0300d89b75c9bf0c32", 54 | "allocation": "2.354", 55 | "impact": 5 56 | }, 57 | { 58 | "project_id": "0x54d4b15fc19bb1d56a611e650d54847ee6fbc24cc19ed3ecb464a4269268270e", 59 | "allocation": "1.526", 60 | "impact": 4 61 | }, 62 | { 63 | "project_id": "0x5886472cf166ffa3671aa8c9bc303f8644e1ad90d67a53aec0f97eb6c9e6844d", 64 | "allocation": "2.635", 65 | "impact": 2 66 | }, 67 | { 68 | "project_id": "0x5a41dd6db9348adfcd8bb0bb0a391a44ac7797094183d536edc8d5677aa985fa", 69 | "allocation": "3.595", 70 | "impact": 3 71 | }, 72 | { 73 | "project_id": "0x6190ca70e5196f582a65d1f73236eff346b72cd922df3305da001bd34c17c348", 74 | "allocation": "1.039", 75 | "impact": 5 76 | }, 77 | { 78 | "project_id": "0x6272a864b7793e1a0ac1b8a2767366f56e8234cb969a7e9f3f21cd70d374a357", 79 | "allocation": "0.838", 80 | "impact": 5 81 | }, 82 | { 83 | "project_id": "0x6605b6f2cb8bad86dd1d82061b0fb7333a6b8d31b9f8c05a2f47cb7d76d0b14f", 84 | "allocation": "2.202", 85 | "impact": 2 86 | }, 87 | { 88 | "project_id": "0x78d76c8cd58f4334478c84da3080a9bd721c8463b1d3ff6d8f838821637419bb", 89 | "allocation": "3.842", 90 | "impact": 2 91 | }, 92 | { 93 | "project_id": "0x7ee54d5d8514a8be9172e4ce0dfb54cc3eba6cf57c88b85e76262dc307178832", 94 | "allocation": "1.283", 95 | "impact": 3 96 | }, 97 | { 98 | "project_id": "0x8f96355ae6573c6c606e2f5a71155344cc42afc853adc4f84933dc7cd00b2e9f", 99 | "allocation": "1.292", 100 | "impact": 1 101 | }, 102 | { 103 | "project_id": "0x902bf20692598d573b33cf7d5006c2c5e488ecc7fbe4b972d67846e9fde5e65a", 104 | "allocation": "4.311", 105 | "impact": 4 106 | }, 107 | { 108 | "project_id": "0x907ab6a3f6b6e724797e52051ac04eb23025462f8d06866ee541c90681e0ec17", 109 | "allocation": "5.359", 110 | "impact": 5 111 | }, 112 | { 113 | "project_id": "0x94af5fe205fbdfd29913aa243514bb6d3e2e1fe121aa2bd984f5eadbedbad751", 114 | "allocation": "3.461", 115 | "impact": 4 116 | }, 117 | { 118 | "project_id": "0x95b0f6f25851da3d3ba17d4cd71e0925b7d234c1db32d5937c846d74ab62fb03", 119 | "allocation": "1.183", 120 | "impact": 4 121 | }, 122 | { 123 | "project_id": "0x9932b50339f36c2327df7eac42965014d561c7401e6fdf50550727ad228f56f2", 124 | "allocation": "0.89", 125 | "impact": 1 126 | }, 127 | { 128 | "project_id": "0x9b70e4a6f08471455d1d674f4e430da7b4fd43848002b47d1b6e1c1a1e0a36db", 129 | "allocation": "5.606", 130 | "impact": 2 131 | }, 132 | { 133 | "project_id": "0xa36ae760edb91ba8bfcc2b52b664fb4731ec09b78033b1bbec9d83d167d590e8", 134 | "allocation": "4.761", 135 | "impact": 3 136 | }, 137 | { 138 | "project_id": "0xa5446a9856ac6e5de47d9a75c9b5633f60a40c07fe23297863b794ca8842984c", 139 | "allocation": "3.477", 140 | "impact": 1 141 | }, 142 | { 143 | "project_id": "0xa61bcee8283f00abfca8890d602ea9542a75c925f43e05d85cb4e017c60f8017", 144 | "allocation": "2.73", 145 | "impact": 4 146 | }, 147 | { 148 | "project_id": "0xbbea5a9a59dc71358d4a4e687630a8a461efc17e56054f36e8c75246520a4199", 149 | "allocation": "2.328", 150 | "impact": 3 151 | }, 152 | { 153 | "project_id": "0xc8a03780dc632e8fad9356662e52723f5e6cff9ba457c0f29e15653c2d26fe7c", 154 | "allocation": "0.511", 155 | "impact": 1 156 | }, 157 | { 158 | "project_id": "0xd33479c5420a69c1ce3c8ba955593a63dcd454feeac43a4de417aa0bb793980a", 159 | "allocation": "3.98", 160 | "impact": 3 161 | }, 162 | { 163 | "project_id": "0xd42d5fa61ac3f9488e7b5c5fd24709d9d2b130750a9e06df868fe0bf3d14b849", 164 | "allocation": "1.53", 165 | "impact": 4 166 | }, 167 | { 168 | "project_id": "0xd890d5c2369e84688d196a6181ddfde1f1fc9d2d3f4e55ee93a3da851145f96f", 169 | "allocation": "1.634", 170 | "impact": 5 171 | }, 172 | { 173 | "project_id": "0xde61c4ad48fd0a8e1e783490aeab9e2d8aa05aaeba1c619e3d520f7e277e461e", 174 | "allocation": "1.229", 175 | "impact": 4 176 | }, 177 | { 178 | "project_id": "0xdfeced1045accaf24826bb678bc6c44624588c1d1b5a56d6b60ad2774fde4352", 179 | "allocation": "3.657", 180 | "impact": 2 181 | }, 182 | { 183 | "project_id": "0xe4ad25cfe18eccc531582634d03b78b03b7115d5fc77de001e735fe178f768f3", 184 | "allocation": "5.127", 185 | "impact": 2 186 | }, 187 | { 188 | "project_id": "0xf013cc101a13131dca5783bb915792e458cc8fc36bbecc3faaaa2aedff2e5f23", 189 | "allocation": "0.012", 190 | "impact": 4 191 | }, 192 | { 193 | "project_id": "0xfc00945848ac2ee9a42c64242e90d612cd16435b46082d4a363c087bd7e4c742", 194 | "allocation": "0.889", 195 | "impact": 2 196 | } 197 | ] -------------------------------------------------------------------------------- /src/rpgf5-data-import/icats-insert.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { projects } from './gsheet'; 3 | 4 | const implicitCatDist = async (prisma: PrismaClient) => { 5 | const projects = await prisma.project.findMany({ 6 | select: { implicitCategory: true }, 7 | where: { 8 | type: 'project', 9 | }, 10 | }); 11 | 12 | const distro = projects.reduce( 13 | (acc, project) => { 14 | if (!project.implicitCategory) acc['null'] = acc['null'] + 1; 15 | else if (project.implicitCategory in acc) 16 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 17 | // @ts-ignore 18 | acc[project.implicitCategory] = acc[project.implicitCategory] + 1; 19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 20 | // @ts-ignore 21 | else acc[project.implicitCategory] = 1; 22 | 23 | return acc; 24 | }, 25 | { null: 0 }, 26 | ); 27 | 28 | return distro; 29 | }; 30 | 31 | export const main = async () => { 32 | const prisma = new PrismaClient({ 33 | datasources: { 34 | db: { 35 | url: process.env.POSTGRES_PRISMA_URL, 36 | }, 37 | }, 38 | }); 39 | 40 | await prisma.$connect(); 41 | 42 | console.log('Connected!'); 43 | 44 | // // const nullPs = await prisma.project.findMany({ 45 | // // select: { name: true }, 46 | // // where: { type: 'project', implicitCategory: null }, 47 | // // }); 48 | 49 | // // console.log(nullPs); 50 | 51 | // for (let i = 0; i < projects.length; i++) { 52 | // const project = projects[i]; 53 | // if (project.isSelected !== 'Approved') continue; 54 | // console.log('Project', i); 55 | // const exists = await prisma.project.findFirst({ 56 | // where: { name: project.name.trim() }, 57 | // }); 58 | 59 | // if (!exists) { 60 | // console.error(`${project.name} not in the db`); 61 | // continue; 62 | // } 63 | 64 | // await prisma.project.update({ 65 | // where: { 66 | // id: exists.id, 67 | // }, 68 | // data: { 69 | // implicitCategory: project['PW subcategory'], 70 | // }, 71 | // }); 72 | // } 73 | 74 | console.log(await implicitCatDist(prisma)); 75 | 76 | await prisma.$disconnect(); 77 | }; 78 | 79 | void main(); 80 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/index copy.ts: -------------------------------------------------------------------------------- 1 | import { projects } from './all-projects-928'; 2 | import { projects as gProjects } from './gsheet'; 3 | 4 | // async function getAllProjects(): Promise { 5 | // const baseUrl = 6 | // 'https://vote.optimism.io/api/v1/retrofunding/rounds/5/projects'; 7 | // const limit = 100; // Maximum allowed limit 8 | // let offset = 0; 9 | // let allProjects: Project[] = []; 10 | // let hasNext = true; 11 | 12 | // while (hasNext) { 13 | // try { 14 | // const response = await axios.get(baseUrl, { 15 | // params: { 16 | // offset, 17 | // limit, 18 | // }, 19 | // headers: { 20 | // Authorization: 'Bearer 80963194-c250-4a37-921a-302bf50dee34', 21 | // }, 22 | // timeout: 30 * 1000, 23 | // }); 24 | 25 | // allProjects = allProjects.concat(response.data.data); 26 | // hasNext = response.data.meta.has_next; 27 | // offset = response.data.meta.next_offset; 28 | // } catch (error) { 29 | // console.error('Error fetching projects:', error); 30 | // break; 31 | // } 32 | // } 33 | 34 | // return allProjects; 35 | // } 36 | 37 | export const main = async () => { 38 | console.log( 39 | projects.filter( 40 | (item) => 41 | !gProjects.find( 42 | (el) => 43 | el['isSelected'] === 'Approved' && 44 | el.name.trim() === item.name.trim(), 45 | ), 46 | ), 47 | ); 48 | }; 49 | 50 | void main(); 51 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { AgoraApiResponse, Project } from './types'; 3 | import { Prisma, PrismaClient } from '@prisma/client'; 4 | import * as fs from 'fs'; 5 | 6 | async function getAllProjects(): Promise { 7 | const baseUrl = 8 | 'https://vote.optimism.io/api/v1/retrofunding/rounds/5/projects'; 9 | const limit = 100; // Maximum allowed limit 10 | let offset = 0; 11 | let allProjects: Project[] = []; 12 | let hasNext = true; 13 | 14 | while (hasNext) { 15 | try { 16 | const response = await axios.get(baseUrl, { 17 | params: { 18 | offset, 19 | limit, 20 | }, 21 | headers: { 22 | Authorization: 'Bearer 80963194-c250-4a37-921a-302bf50dee34', 23 | }, 24 | timeout: 30 * 1000, 25 | }); 26 | 27 | allProjects = allProjects.concat(response.data.data); 28 | hasNext = response.data.meta.has_next; 29 | offset = response.data.meta.next_offset; 30 | } catch (error) { 31 | console.error('Error fetching projects:', error); 32 | break; 33 | } 34 | } 35 | 36 | return allProjects; 37 | } 38 | 39 | const getSpace = (): Prisma.SpaceUncheckedCreateInput => ({ 40 | title: 'Optimism (OP)', 41 | description: 42 | 'OP is a Layer 2 Optimistic Rollup network designed to utilize the strong security guarantees of Ethereum while reducing its cost and latency.', 43 | }); 44 | 45 | const getPoll = (): Prisma.PollUncheckedCreateInput => ({ 46 | title: 'RetroPGF 5', 47 | endsAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // in 60 days 48 | spaceId: 1, 49 | }); 50 | 51 | export const main = async () => { 52 | const categories: Record = {}; 53 | 54 | const nonSpamProjects = await getAllProjects(); 55 | 56 | console.log('projects length', nonSpamProjects.length); 57 | 58 | for (let i = 0; i < nonSpamProjects.length; i++) { 59 | const project = nonSpamProjects[i]; 60 | 61 | if (!(project['applicationCategory'] in categories)) { 62 | categories[project['applicationCategory']] = 1; 63 | } else { 64 | categories[project['applicationCategory']] = 65 | categories[project['applicationCategory']] + 1; 66 | } 67 | } 68 | 69 | console.log(categories); 70 | 71 | // fs.writeFile( 72 | // 'all-projects-930.json', 73 | // JSON.stringify(nonSpamProjects), 74 | // (err) => { 75 | // if (err) { 76 | // console.error('Error writing file', err); 77 | // } else { 78 | // console.log('JSON file saved successfully'); 79 | // } 80 | // }, 81 | // ); 82 | 83 | const prisma = new PrismaClient({ 84 | datasources: { 85 | db: { 86 | url: process.env.POSTGRES_PRISMA_URL, 87 | }, 88 | }, 89 | }); 90 | 91 | await prisma.$connect(); 92 | 93 | console.log('Connected!'); 94 | 95 | const space = getSpace(); 96 | await prisma.space.create({ 97 | data: space, 98 | }); 99 | 100 | // add poll 101 | const poll = getPoll(); 102 | await prisma.poll.create({ 103 | data: poll, 104 | }); 105 | 106 | // // add categories 107 | 108 | await prisma.project.createMany({ 109 | data: Object.keys(categories).map((category) => ({ 110 | type: 'collection', 111 | pollId: 1, 112 | name: category, 113 | description: `Description for ${category}`, 114 | metadata: '', 115 | })), 116 | }); 117 | 118 | for (let i = 0; i < Object.keys(categories).length; i++) { 119 | console.log('Now doing category #', i); 120 | const catName = Object.keys(categories)[i]; 121 | const category = await prisma.project.findFirst({ 122 | select: { id: true }, 123 | where: { 124 | name: catName, 125 | type: 'collection', 126 | }, 127 | }); 128 | 129 | if (!category) 130 | throw new Error( 131 | `No category available for this category name ${catName}`, 132 | ); 133 | 134 | const categoryProjects = nonSpamProjects.filter( 135 | (el) => el['applicationCategory'] === catName, 136 | ); 137 | 138 | await prisma.project.createMany({ 139 | data: categoryProjects.map((project) => ({ 140 | type: 'project', 141 | parentId: category.id, 142 | pollId: 1, 143 | image: project.profileAvatarUrl, 144 | name: `${project.name}`.trim(), 145 | description: project.description, 146 | // contributionDescription: project.contributionDescription, 147 | // shortDescription: project['Short description'], 148 | RPGF5Id: project.id, 149 | metadata: JSON.stringify(project), 150 | })), 151 | }); 152 | } 153 | 154 | // await addRpgf4IdToCollections(prisma); 155 | 156 | // // for (const metric of metricsArray) { 157 | // // await addMetricsId(prisma, metric.project_id); 158 | // // } 159 | 160 | // await addImages(prisma); 161 | 162 | // // await insertTopCollections(prisma); 163 | 164 | // // await insertMoonCollections(prisma); 165 | 166 | // // await insertProjects(prisma); 167 | 168 | // // findErigonI(); 169 | // // printCategories(); 170 | 171 | await prisma.$disconnect(); 172 | }; 173 | 174 | void main(); 175 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/submit.ts: -------------------------------------------------------------------------------- 1 | // import { projects } from './osrad'; 2 | // import * as fs from 'fs'; 3 | // // async function getAllProjects(): Promise { 4 | // // const baseUrl = 5 | // // 'https://vote.optimism.io/api/v1/retrofunding/rounds/5/projects'; 6 | // // const limit = 100; // Maximum allowed limit 7 | // // let offset = 0; 8 | // // let allProjects: Project[] = []; 9 | // // let hasNext = true; 10 | 11 | // // while (hasNext) { 12 | // // try { 13 | // // const response = await axios.get(baseUrl, { 14 | // // params: { 15 | // // offset, 16 | // // limit, 17 | // // }, 18 | // // headers: { 19 | // // Authorization: 'Bearer 80963194-c250-4a37-921a-302bf50dee34', 20 | // // }, 21 | // // timeout: 30 * 1000, 22 | // // }); 23 | 24 | // // allProjects = allProjects.concat(response.data.data); 25 | // // hasNext = response.data.meta.has_next; 26 | // // offset = response.data.meta.next_offset; 27 | // // } catch (error) { 28 | // // console.error('Error fetching projects:', error); 29 | // // break; 30 | // // } 31 | // // } 32 | 33 | // // return allProjects; 34 | // // } 35 | 36 | export type AgoraBallotPost = { 37 | projects: { 38 | project_id: string; 39 | allocation: string; 40 | impact: number; 41 | }[]; 42 | }; 43 | 44 | // const createFakeDistribution = (total: number) => { 45 | // const distribution = []; 46 | 47 | // for (let i = 0; i < total; i++) { 48 | // distribution.push(Math.random()); 49 | // } 50 | 51 | // const initialSum = distribution.reduce((a, b) => a + b, 0); 52 | 53 | // const coefficient = 100 / initialSum; 54 | 55 | // return distribution.map((el) => Math.round(el * coefficient * 1000) / 1000); 56 | // }; 57 | 58 | // export const main = async () => { 59 | // const dist = createFakeDistribution(projects.length); 60 | // const ballot = projects.map((item, index) => ({ 61 | // project_id: item.id, 62 | // allocation: `${dist[index]}`, 63 | // impact: Math.round(Math.random() * 4) + 1, 64 | // })); 65 | 66 | // const jsonString = JSON.stringify(ballot, null, 2); 67 | 68 | // fs.writeFile('ballot.json', jsonString, (err) => { 69 | // if (err) { 70 | // console.error('Error writing file', err); 71 | // } else { 72 | // console.log('JSON file saved successfully'); 73 | // } 74 | // }); 75 | // }; 76 | 77 | // void main(); 78 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/temp-total.ts: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | userAddress: '0x43664B06911bd2f1F7Ee76CF6A82786106005A4f', 4 | progress: 0.3374233128834356, 5 | }, 6 | { 7 | userAddress: '0xA1179f64638adb613DDAAc32D918EB6BEB824104', 8 | progress: 0.3392857142857143, 9 | }, 10 | { 11 | userAddress: '0x7F37e3008207C27360b20ABCFB5fdCc8e37596B8', 12 | progress: 0.3380281690140845, 13 | }, 14 | { 15 | userAddress: '0xA4D506434445Bb7303eA34A07bc38484cdC64a95', 16 | progress: 0.34285714285714286, 17 | }, 18 | { 19 | userAddress: '0x839395e20bbB182fa440d08F850E6c7A8f6F0780', 20 | progress: 0.33962264150943394, 21 | }, 22 | { 23 | userAddress: '0x6D97d65aDfF6771b31671443a6b9512104312d3D', 24 | progress: 0.3333333333333333, 25 | }, 26 | { 27 | userAddress: '0x33d0EdC9d1F0407eD7bDD0637dE74cfdd24a56eE', 28 | progress: 0.336734693877551, 29 | }, 30 | { 31 | userAddress: '0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c', 32 | progress: 0.3333333333333333, 33 | }, 34 | { 35 | userAddress: '0x7899d9b1181cbB427b0b1BE0684C096C260F7474', 36 | progress: 0.3333333333333333, 37 | }, 38 | { 39 | userAddress: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', 40 | progress: 0.3380281690140845, 41 | }, 42 | { 43 | userAddress: '0x8Eb9e5E5375b72eE7c5cb786CE8564D854C26A86', 44 | progress: 0.3372093023255814, 45 | }, 46 | { 47 | userAddress: '0xe53e89d978Ff1da716f80BaA6E6D8B3FA23f2284', 48 | progress: 0.3357664233576642, 49 | }, 50 | { 51 | userAddress: '0x00409fC839a2Ec2e6d12305423d37Cd011279C09', 52 | progress: 0.3333333333333333, 53 | }, 54 | { 55 | userAddress: '0x316131DC685A63B1dbC8E0Fc6B893ec51CF159DA', 56 | progress: 0.34177215189873417, 57 | }, 58 | { 59 | userAddress: '0x826976d7C600d45FB8287CA1d7c76FC8eb732030', 60 | progress: 0.3333333333333333, 61 | }, 62 | { 63 | userAddress: '0x1543c3446f436576417490a647987199685e0b8d', 64 | progress: 0.33613445378151263, 65 | }, 66 | { 67 | userAddress: '0x5C30F1273158318D3DC8FFCf991421f69fD3B77d', 68 | progress: 0.3409090909090909, 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/temp.ts: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | userAddress: '0x839395e20bbB182fa440d08F850E6c7A8f6F0780', 4 | progress: 0.33962264150943394, 5 | }, 6 | { 7 | userAddress: '0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c', 8 | progress: 0.3333333333333333, 9 | }, 10 | { 11 | userAddress: '0x7899d9b1181cbB427b0b1BE0684C096C260F7474', 12 | progress: 0.3333333333333333, 13 | }, 14 | { 15 | userAddress: '0x8Eb9e5E5375b72eE7c5cb786CE8564D854C26A86', 16 | progress: 0.3372093023255814, 17 | }, 18 | { 19 | userAddress: '0xe53e89d978Ff1da716f80BaA6E6D8B3FA23f2284', 20 | progress: 0.3357664233576642, 21 | }, 22 | { 23 | userAddress: '0x00409fC839a2Ec2e6d12305423d37Cd011279C09', 24 | progress: 0.3333333333333333, 25 | }, 26 | { 27 | userAddress: '0x826976d7C600d45FB8287CA1d7c76FC8eb732030', 28 | progress: 0.3333333333333333, 29 | }, 30 | { 31 | userAddress: '0x5C30F1273158318D3DC8FFCf991421f69fD3B77d', 32 | progress: 0.3409090909090909, 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/types.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: string; 3 | applicationId: string; 4 | projectId: string; 5 | category: string; 6 | applicationCategory: string; 7 | organization: { 8 | name: string; 9 | description: string; 10 | organizationAvatarUrl: string; 11 | organizationCoverImageUrl: string | null; 12 | socialLinks: { 13 | website: string[]; 14 | farcaster: string[]; 15 | twitter: string | null; 16 | mirror: string | null; 17 | }; 18 | team: string[]; 19 | }; 20 | name: string; 21 | description: string; 22 | profileAvatarUrl: string; 23 | projectCoverImageUrl: string; 24 | socialLinks: { 25 | website: string[]; 26 | farcaster: string[]; 27 | twitter: string | null; 28 | mirror: string | null; 29 | }; 30 | team: string[]; 31 | github: Array<{ 32 | url: string; 33 | name: string | null; 34 | description: string | null; 35 | }>; 36 | packages: any[]; 37 | links: Array<{ 38 | url: string; 39 | name: string; 40 | description: string; 41 | }>; 42 | contracts: any[]; 43 | grantsAndFunding: { 44 | ventureFunding: any[]; 45 | grants: Array<{ 46 | grant: string | null; 47 | link: string | null; 48 | amount: string; 49 | date: string; 50 | details: string | null; 51 | }>; 52 | revenue: any[]; 53 | }; 54 | pricingModel: string; 55 | impactStatement: { 56 | category: string; 57 | subcategory: string[]; 58 | statement: Array<{ 59 | answer: string; 60 | question: string; 61 | }>; 62 | }; 63 | } 64 | 65 | export interface AgoraApiResponse { 66 | meta: { 67 | has_next: boolean; 68 | total_returned: number; 69 | next_offset: number; 70 | }; 71 | data: Project[]; 72 | } 73 | -------------------------------------------------------------------------------- /src/rpgf5-data-import/update-metadata.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import axios from 'axios'; 3 | import { AgoraApiResponse, Project } from './types'; 4 | import * as fs from 'fs'; 5 | 6 | async function getAllProjects(): Promise { 7 | const baseUrl = 8 | 'https://vote.optimism.io/api/v1/retrofunding/rounds/5/projects'; 9 | const limit = 100; // Maximum allowed limit 10 | let offset = 0; 11 | let allProjects: Project[] = []; 12 | let hasNext = true; 13 | 14 | while (hasNext) { 15 | try { 16 | const response = await axios.get(baseUrl, { 17 | params: { 18 | offset, 19 | limit, 20 | }, 21 | headers: { 22 | Authorization: 'Bearer 80963194-c250-4a37-921a-302bf50dee34', 23 | }, 24 | timeout: 30 * 1000, 25 | }); 26 | 27 | allProjects = allProjects.concat(response.data.data); 28 | hasNext = response.data.meta.has_next; 29 | offset = response.data.meta.next_offset; 30 | } catch (error) { 31 | console.error('Error fetching projects:', error); 32 | break; 33 | } 34 | } 35 | 36 | return allProjects; 37 | } 38 | 39 | export const main = async () => { 40 | const prisma = new PrismaClient({ 41 | datasources: { 42 | db: { 43 | url: process.env.POSTGRES_PRISMA_URL, 44 | }, 45 | }, 46 | }); 47 | 48 | await prisma.$connect(); 49 | 50 | console.log('Connected!'); 51 | 52 | const projects = await getAllProjects(); 53 | 54 | // const nullPs = await prisma.project.findMany({ 55 | // select: { name: true }, 56 | // where: { type: 'project', implicitCategory: null }, 57 | // }); 58 | 59 | // console.log(nullPs); 60 | 61 | // fs.writeFile('all-projects.json', JSON.stringify(projects), (err) => { 62 | // if (err) { 63 | // console.error('Error writing file', err); 64 | // } else { 65 | // console.log('JSON file saved successfully'); 66 | // } 67 | // }); 68 | 69 | for (let i = 0; i < projects.length; i++) { 70 | console.log('Project', i); 71 | const project = projects[i]; 72 | const exists = await prisma.project.findFirst({ 73 | where: { OR: [{ name: project.name.trim(), RPGF5Id: project.id }] }, 74 | }); 75 | 76 | if (!exists) { 77 | console.log( 78 | `Project ${project.name} with id: ${project.id} doens't exist`, 79 | ); 80 | } 81 | 82 | if (exists) { 83 | await prisma.project.update({ 84 | where: { 85 | id: exists.id, 86 | }, 87 | data: { 88 | name: exists.name, 89 | }, 90 | }); 91 | } 92 | } 93 | 94 | // console.log(await implicitCatDist(prisma)); 95 | 96 | await prisma.$disconnect(); 97 | }; 98 | 99 | void main(); 100 | -------------------------------------------------------------------------------- /src/user/dto/ConnectFlowDTOs.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsEthereumAddress } from 'class-validator'; 2 | 3 | export class StoreBadgesAndIdentityDTO { 4 | @IsEthereumAddress() 5 | @IsDefined() 6 | mainAddress: string; 7 | 8 | @IsDefined() 9 | signature: string; 10 | 11 | @IsDefined() 12 | identity: string; 13 | } 14 | 15 | export class StoreBadgesDTO { 16 | @IsEthereumAddress() 17 | @IsDefined() 18 | mainAddress: string; 19 | 20 | @IsDefined() 21 | signature: string; 22 | } 23 | 24 | export class StoreIdentityDTO { 25 | @IsDefined() 26 | identity: string; 27 | } 28 | export class GetBadgesDTO { 29 | @IsEthereumAddress() 30 | @IsDefined() 31 | address: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/user/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | ConflictException, 5 | Controller, 6 | ForbiddenException, 7 | Get, 8 | InternalServerErrorException, 9 | Logger, 10 | Post, 11 | Query, 12 | Req, 13 | UnauthorizedException, 14 | UseGuards, 15 | Headers, 16 | } from '@nestjs/common'; 17 | 18 | import { AuthedReq } from 'src/utils/types/AuthedReq.type'; 19 | import { 20 | GetBadgesDTO, 21 | StoreBadgesAndIdentityDTO, 22 | StoreBadgesDTO, 23 | StoreIdentityDTO, 24 | } from './dto/ConnectFlowDTOs'; 25 | import { getBadges } from 'src/utils/badges/readBadges'; 26 | import { verifySignature } from 'src/utils/badges'; 27 | import { PrismaService } from 'src/prisma.service'; 28 | import { AuthGuard } from 'src/auth/auth.guard'; 29 | import { Prisma } from '@prisma/client'; 30 | import { snapshots } from 'src/utils/badges/snapshot'; 31 | 32 | @Controller({ path: 'user' }) 33 | export class UsersController { 34 | private readonly logger = new Logger(UsersController.name); 35 | constructor(private readonly prismaService: PrismaService) {} 36 | 37 | @UseGuards(AuthGuard) 38 | @Post('/store-badges-identity') 39 | async storeBadgesAndIdentity( 40 | @Req() { userId }: AuthedReq, 41 | @Body() { mainAddress, signature, identity }: StoreBadgesAndIdentityDTO, 42 | ) { 43 | if ( 44 | !(await verifySignature( 45 | 'Sign this message to generate your Semaphore identity.', 46 | signature, 47 | mainAddress, 48 | )) 49 | ) 50 | throw new UnauthorizedException('Signature invalid'); 51 | 52 | // if (!verifyIdentity(identity)) 53 | // throw new UnauthorizedException('Invalid identity format'); 54 | 55 | const user = await this.prismaService.user.findUnique({ 56 | select: { badges: true, identity: true, opAddress: true }, 57 | where: { id: userId }, 58 | }); 59 | 60 | if (!user) throw new InternalServerErrorException("User doesn't exist"); 61 | 62 | if (user.identity?.valueOf() || user.badges?.valueOf() || user.opAddress) 63 | throw new ForbiddenException('User has already connected'); 64 | 65 | const badges = await getBadges(snapshots, mainAddress); 66 | 67 | try { 68 | await this.prismaService.user.update({ 69 | where: { 70 | id: userId, 71 | }, 72 | data: { 73 | identity, 74 | badges: badges || {}, 75 | opAddress: mainAddress, 76 | }, 77 | }); 78 | } catch (e: unknown) { 79 | if ( 80 | e instanceof Prisma.PrismaClientKnownRequestError && 81 | e.code === 'P2002' 82 | ) { 83 | throw new ConflictException('This eth address is already connected'); 84 | } 85 | } 86 | 87 | return 'success'; 88 | } 89 | 90 | @UseGuards(AuthGuard) 91 | @Post('/store-badges') 92 | async storeBadges( 93 | @Req() { userId }: AuthedReq, 94 | @Body() { mainAddress, signature }: StoreBadgesDTO, 95 | ) { 96 | if ( 97 | !(await verifySignature( 98 | 'Sign this message to generate your Semaphore identity.', 99 | signature, 100 | mainAddress, 101 | )) 102 | ) 103 | throw new UnauthorizedException('Signature invalid'); 104 | 105 | const badges = await getBadges(snapshots, mainAddress); 106 | 107 | const user = await this.prismaService.user.findUnique({ 108 | select: { badges: true, identity: true }, 109 | where: { id: userId }, 110 | }); 111 | 112 | if (!user) throw new InternalServerErrorException("User doesn't exist"); 113 | 114 | if (user.badges?.valueOf()) 115 | throw new ForbiddenException('User has already connected'); 116 | 117 | if (!user.identity?.valueOf) 118 | throw new BadRequestException('You need to insert your identity first'); 119 | 120 | try { 121 | await this.prismaService.user.update({ 122 | where: { 123 | id: userId, 124 | }, 125 | data: { 126 | badges: badges || {}, 127 | opAddress: mainAddress, 128 | }, 129 | }); 130 | } catch (e: unknown) { 131 | if ( 132 | e instanceof Prisma.PrismaClientKnownRequestError && 133 | e.code === 'P2002' 134 | ) { 135 | throw new ConflictException('This eth address is already connected'); 136 | } 137 | } 138 | 139 | return badges; 140 | } 141 | 142 | @UseGuards(AuthGuard) 143 | @Get('/badges') 144 | async getBadges(@Req() { userId }: AuthedReq) { 145 | const res = await this.prismaService.user.findUnique({ 146 | select: { badges: true }, 147 | where: { id: userId }, 148 | }); 149 | 150 | return res?.badges || null; 151 | } 152 | 153 | @UseGuards(AuthGuard) 154 | @Get('/identity') 155 | async getIdentity(@Req() { userId }: AuthedReq) { 156 | const res = await this.prismaService.user.findUnique({ 157 | select: { identity: true }, 158 | where: { id: userId }, 159 | }); 160 | 161 | return res?.identity || null; 162 | } 163 | 164 | // @Get('/public/duplicates') 165 | // async getDuplicates() { 166 | // snapshotPoints.forEach((point, index) => { 167 | // const newIndex = snapshotPoints.findIndex( 168 | // (el, i2) => 169 | // index !== i2 && point.User.toLowerCase() === el.User.toLowerCase(), 170 | // ); 171 | 172 | // if (newIndex !== -1) console.log(snapshotPoints[newIndex]); 173 | // }); 174 | // } 175 | 176 | @Get('/public/badges') 177 | async getPublicBadges(@Query() { address }: GetBadgesDTO) { 178 | const badges = await getBadges(snapshots, address); 179 | 180 | return badges || {}; 181 | } 182 | 183 | @Get('/smart-wallet-badges') 184 | async getSmartWalletBadges(@Headers('authorization') authorization: string) { 185 | if (authorization !== process.env.Smart_Wallet_Badges_Authorization) 186 | throw new UnauthorizedException(); 187 | const userBadges = await this.prismaService.user.findMany({ 188 | select: { 189 | address: true, 190 | badges: true, 191 | }, 192 | where: { 193 | badges: { 194 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 195 | // @ts-ignore 196 | not: null, 197 | }, 198 | }, 199 | }); 200 | 201 | return userBadges; 202 | } 203 | 204 | @UseGuards(AuthGuard) 205 | @Post('/store-identity') 206 | async storeIdentity( 207 | @Req() { userId }: AuthedReq, 208 | @Body() { identity }: StoreIdentityDTO, 209 | ) { 210 | // if (!verifyIdentity(identity)) 211 | // throw new UnauthorizedException('Invalid identity format'); 212 | 213 | const user = await this.prismaService.user.findUnique({ 214 | select: { badges: true, identity: true }, 215 | where: { id: userId }, 216 | }); 217 | 218 | if (!user) throw new InternalServerErrorException("User doesn't exist"); 219 | 220 | if (user.identity?.valueOf()) 221 | throw new ForbiddenException('User has already connected'); 222 | 223 | try { 224 | await this.prismaService.user.update({ 225 | where: { 226 | id: userId, 227 | }, 228 | data: { 229 | identity, 230 | }, 231 | }); 232 | } catch (e: unknown) { 233 | if ( 234 | e instanceof Prisma.PrismaClientKnownRequestError && 235 | e.code === 'P2002' 236 | ) { 237 | throw new ConflictException('This eth address is already connected'); 238 | } 239 | } 240 | 241 | return 'success'; 242 | } 243 | 244 | @UseGuards(AuthGuard) 245 | @Post('/continue-guest') 246 | async continueGuest(@Req() { userId }: AuthedReq) { 247 | await this.prismaService.user.update({ 248 | where: { 249 | id: userId, 250 | }, 251 | data: { 252 | identity: {}, 253 | badges: {}, 254 | }, 255 | }); 256 | 257 | return 'success'; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/user/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | import { UsersController } from './users.controller'; 5 | import { AuthModule } from 'src/auth/auth.module'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => AuthModule)], 9 | providers: [UsersService, PrismaService], 10 | controllers: [UsersController], 11 | exports: [UsersService], 12 | }) 13 | export class UsersModule {} 14 | -------------------------------------------------------------------------------- /src/user/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { User } from '@prisma/client'; 3 | import { PrismaService } from 'src/prisma.service'; 4 | import { hashData } from 'src/utils'; 5 | 6 | @Injectable() 7 | export class UsersService { 8 | private readonly logger = new Logger(UsersService.name); 9 | constructor(private prismaService: PrismaService) {} 10 | 11 | create = async ({ 12 | address, 13 | isBadgeHolder, 14 | }: { 15 | address: string; 16 | isBadgeHolder: boolean; 17 | }): Promise => { 18 | return this.prismaService.user.create({ 19 | data: { address, isBadgeHolder: isBadgeHolder ? 1 : 0 }, 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/badges/index.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | /** 4 | * Verifies that a signature comes from a given Ethereum address. 5 | * 6 | * @param message - The original message that was signed. 7 | * @param signature - The signature in hexadecimal format. 8 | * @param claimedAddress - The Ethereum address claimed to have signed the message. 9 | * @returns - A boolean indicating whether the signature is valid. 10 | */ 11 | export async function verifySignature( 12 | message: string, 13 | signature: string, 14 | claimedAddress: string, 15 | ): Promise { 16 | // Hash the message with the Ethereum prefix 17 | const messageHash = ethers.hashMessage(message); 18 | 19 | // Recover the address from the signature and hashed message 20 | const recoveredAddress = ethers.recoverAddress(messageHash, signature); 21 | 22 | // Compare the recovered address with the claimed address 23 | return recoveredAddress.toLowerCase() === claimedAddress.toLowerCase(); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/badges/readBadges.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { RawSnapshotPoint } from './type'; 3 | 4 | const medalTypes = [ 5 | 'Bronze', 6 | 'Diamond', 7 | 'Platinum', 8 | 'Gold', 9 | 'Silver', 10 | 'Whale', 11 | ] as const; 12 | 13 | export type BadgeData = { 14 | holderPoints?: number; 15 | delegatePoints?: number; 16 | holderAmount?: number; 17 | delegateAmount?: number; 18 | recipientsPoints?: 1; 19 | badgeholderPoints?: 1; 20 | holderType?: (typeof medalTypes)[number]; 21 | delegateType?: (typeof medalTypes)[number]; 22 | }; 23 | 24 | // Import the ethers library 25 | 26 | // Define an async function to resolve the ENS name 27 | async function reverseENSLookup(address: string) { 28 | const dkey = process.env.DKEY; 29 | // Connect to the Ethereum network (mainnet) 30 | const provider = new ethers.JsonRpcProvider( 31 | dkey 32 | ? `https://lb.drpc.org/ogrpc?network=ethereum&dkey=${dkey}` 33 | : 'https://eth.llamarpc.com', 34 | ); 35 | 36 | try { 37 | // Resolve the ENS name to an Ethereum address 38 | const ensName = await provider.lookupAddress(address); 39 | return ensName; 40 | } catch (error) { 41 | return null; 42 | } 43 | } 44 | 45 | function isNumeric(str: string): boolean { 46 | return /^\d+$/.test(str); // Matches positive integers 47 | } 48 | 49 | // Function to get badge data by user address from the Map 50 | export const getBadges = async ( 51 | points: RawSnapshotPoint[], 52 | userAddress: string, 53 | ) => { 54 | let row: RawSnapshotPoint | undefined = undefined; 55 | const ensName = await reverseENSLookup(userAddress); 56 | const temp = points.find( 57 | (el) => el.User.toLowerCase() === userAddress.toLowerCase(), 58 | ); 59 | 60 | if (!temp) { 61 | if (ensName) { 62 | const temp2 = points.find( 63 | (el) => el.User.toLowerCase() === ensName.toLowerCase(), 64 | ); 65 | if (temp2) row = temp2; 66 | } 67 | } else { 68 | row = temp; 69 | } 70 | 71 | if (!row) return undefined; 72 | 73 | const filtered: BadgeData = {}; 74 | let key: keyof typeof row; 75 | for (key in row) { 76 | if (key === 'User') continue; 77 | const value = row[key]; 78 | if (value === 0 || value === null || value === '0' || value === 'null') 79 | continue; 80 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 81 | // @ts-ignore 82 | if (isNumeric(value)) filtered[key] = Number(value); 83 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 84 | // @ts-ignore 85 | else filtered[key] = value; 86 | } 87 | 88 | return filtered; 89 | }; 90 | -------------------------------------------------------------------------------- /src/utils/badges/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { productionSnapshots } from './production-snapshot'; 2 | import { testAddresses } from './test-snapshots'; 3 | 4 | export const snapshots = 5 | process.env.NODE_ENV === 'production' 6 | ? [...productionSnapshots] 7 | : [...productionSnapshots, ...testAddresses]; 8 | -------------------------------------------------------------------------------- /src/utils/badges/snapshotQa.ts: -------------------------------------------------------------------------------- 1 | // import { RawSnapshotPoint } from './type'; 2 | // import { productionSnapshots } from './production-snapshot2'; 3 | // import { snapshots707 } from './snapshots707'; 4 | // import * as fs from 'fs' 5 | // import { PrismaClient } from '@prisma/client'; 6 | // import { removeds } from './removeds'; 7 | 8 | // // function compareSnapshots( 9 | // // snapshot1: RawSnapshotPoint, 10 | // // snapshot2: RawSnapshotPoint, 11 | // // ): boolean { 12 | // // // Iterate over the keys of the first snapshot 13 | // // for (const key in snapshot1) { 14 | // // // Compare the current key's value in both snapshots 15 | // // // Convert values to string to handle number vs string comparison correctly 16 | // // const value1 = snapshot1[key as keyof RawSnapshotPoint]; 17 | // // const value2 = snapshot2[key as keyof RawSnapshotPoint]; 18 | 19 | // // if (String(value1) !== String(value2)) { 20 | // // // Values differ, return true 21 | // // return true; 22 | // // } 23 | // // } 24 | 25 | // // // No differences found, return false 26 | // // return false; 27 | // // } 28 | 29 | // function findRepetitions( 30 | // snapshots: RawSnapshotPoint[], 31 | // snapshots2: RawSnapshotPoint[], 32 | // ) { 33 | // // 764 34 | // let count = 0; 35 | // let countNotR = 0; 36 | // const addresses : string[] = []; 37 | // snapshots.forEach((el, index) => { 38 | // const exists = snapshots2.findIndex( 39 | // (item, i2) => item.User.toLowerCase() === el.User.toLowerCase(), 40 | // ); 41 | 42 | // if (exists === -1) { 43 | // // console.log('Count:', count++); 44 | // // console.log('Item found', el); 45 | // addresses.push(el.User); 46 | // if ( 47 | // Number(el.badgeholderPoints) > 0 || 48 | // el.delegatePoints > 0 || 49 | // el.holderPoints > 0 50 | // ) 51 | // countNotR++; 52 | // } 53 | // }); 54 | 55 | // console.log('Not recepients', countNotR); 56 | // console.log('Addresses', addresses); 57 | // } 58 | 59 | // findRepetitions(productionSnapshots, snapshots707) 60 | 61 | 62 | // // export const findRemovedRecipients = async () => { 63 | // // const prisma = new PrismaClient({ 64 | // // datasources: { 65 | // // db: { 66 | // // url: process.env.POSTGRES_PRISMA_URL, 67 | // // }, 68 | // // }, 69 | // // }); 70 | 71 | // // await prisma.$connect(); 72 | 73 | // // const users = await prisma.user.findMany({ 74 | // // where: { 75 | // // opAddress: { 76 | // // in: removeds, 77 | // // } 78 | // // } 79 | // // }) 80 | 81 | // // console.log(users) 82 | 83 | 84 | 85 | // // await prisma.$disconnect(); 86 | 87 | // // } 88 | 89 | // // findRemovedRecipients() 90 | -------------------------------------------------------------------------------- /src/utils/badges/test-snapshots.ts: -------------------------------------------------------------------------------- 1 | import { RawSnapshotPoint } from './type'; 2 | 3 | export const testAddresses: RawSnapshotPoint[] = [ 4 | { 5 | User: '0x316131DC685A63B1dbC8E0Fc6B893ec51CF159DA', 6 | holderPoints: 0, 7 | delegatePoints: 0, 8 | recipientsPoints: 1, 9 | badgeholderPoints: 0, 10 | holderType: null, 11 | delegateType: null, 12 | holderAmount: 0, 13 | delegateAmount: 0, 14 | }, 15 | { 16 | User: '0x393053056EB678EA95CBc67CB7E1198184984707', 17 | holderPoints: 0, 18 | delegatePoints: 0, 19 | recipientsPoints: 1, 20 | badgeholderPoints: 0, 21 | holderType: null, 22 | delegateType: null, 23 | holderAmount: 0, 24 | delegateAmount: 0, 25 | }, 26 | { 27 | User: '0xc0f2A154abA3f12D71AF25e87ca4f225B9C52203', 28 | holderPoints: 0, 29 | delegatePoints: 0, 30 | recipientsPoints: 1, 31 | badgeholderPoints: 0, 32 | holderType: null, 33 | delegateType: null, 34 | holderAmount: 0, 35 | delegateAmount: 0, 36 | }, 37 | { 38 | User: '0xcd192b61a8Dd586A97592555c1f5709e032F2505', 39 | holderPoints: 0, 40 | delegatePoints: 0, 41 | recipientsPoints: 1, 42 | badgeholderPoints: 0, 43 | holderType: null, 44 | delegateType: null, 45 | holderAmount: 0, 46 | delegateAmount: 0, 47 | }, 48 | { 49 | User: '0xA1179f64638adb613DDAAc32D918EB6BEB824104', 50 | holderPoints: 0, 51 | delegatePoints: 0, 52 | recipientsPoints: 1, 53 | badgeholderPoints: 0, 54 | holderType: null, 55 | delegateType: null, 56 | holderAmount: 0, 57 | delegateAmount: 0, 58 | }, 59 | { 60 | User: '0x6eb78c56F639b3d161456e9f893c8e8aD9d754F0', 61 | holderPoints: 0, 62 | delegatePoints: 0, 63 | recipientsPoints: 1, 64 | badgeholderPoints: 0, 65 | holderType: null, 66 | delegateType: null, 67 | holderAmount: 0, 68 | delegateAmount: 0, 69 | }, 70 | { 71 | User: '0xD5db3F8B0a236176587460dC85F0fC5705D78477', 72 | holderPoints: 0, 73 | delegatePoints: 0, 74 | recipientsPoints: 1, 75 | badgeholderPoints: 0, 76 | holderType: null, 77 | delegateType: null, 78 | holderAmount: 0, 79 | delegateAmount: 0, 80 | }, 81 | { 82 | User: '0xe1e5dcbbc95aabe80e2f9c65c7a2cef85daf61c4', 83 | holderPoints: 0, 84 | delegatePoints: 0, 85 | recipientsPoints: 1, 86 | badgeholderPoints: 0, 87 | holderType: null, 88 | delegateType: null, 89 | holderAmount: 0, 90 | delegateAmount: 0, 91 | }, 92 | { 93 | User: '0x33878e070db7f70D2953Fe0278Cd32aDf8104572', 94 | holderPoints: 0, 95 | delegatePoints: 0, 96 | recipientsPoints: 1, 97 | badgeholderPoints: 0, 98 | holderType: null, 99 | delegateType: null, 100 | holderAmount: 0, 101 | delegateAmount: 0, 102 | }, 103 | { 104 | User: '0xB9573982875b83aaDc1296726E2ae77D13D9B98F', 105 | holderPoints: 0, 106 | delegatePoints: 0, 107 | recipientsPoints: 1, 108 | badgeholderPoints: 0, 109 | holderType: null, 110 | delegateType: null, 111 | holderAmount: 0, 112 | delegateAmount: 0, 113 | }, 114 | { 115 | User: '0x9FE099C5234E873551Fec5c7dd06E5213360A46c', 116 | holderPoints: 0, 117 | delegatePoints: 0, 118 | recipientsPoints: 1, 119 | badgeholderPoints: 0, 120 | holderType: null, 121 | delegateType: null, 122 | holderAmount: 0, 123 | delegateAmount: 0, 124 | }, 125 | { 126 | User: '0x44AC194359fA44eCe6Cb2E53E8c90547BCCb95a0', 127 | holderPoints: 0, 128 | delegatePoints: 0, 129 | recipientsPoints: 1, 130 | badgeholderPoints: 0, 131 | holderType: null, 132 | delegateType: null, 133 | holderAmount: 0, 134 | delegateAmount: 0, 135 | }, 136 | { 137 | User: '0x7F37e3008207C27360b20ABCFB5fdCc8e37596B8', 138 | holderPoints: 0, 139 | delegatePoints: 0, 140 | recipientsPoints: 1, 141 | badgeholderPoints: 0, 142 | holderType: null, 143 | delegateType: null, 144 | holderAmount: 0, 145 | delegateAmount: 0, 146 | }, 147 | { 148 | User: '0x871Cd6353B803CECeB090Bb827Ecb2F361Db81AB', 149 | holderPoints: 0, 150 | delegatePoints: 0, 151 | recipientsPoints: 1, 152 | badgeholderPoints: 0, 153 | holderType: null, 154 | delegateType: null, 155 | holderAmount: 0, 156 | delegateAmount: 0, 157 | }, 158 | { 159 | User: '0x523E41A134Ab0999F2dC844eA02d9b53cC28fD1a', 160 | holderPoints: 3, 161 | delegatePoints: 1, 162 | recipientsPoints: 1, 163 | badgeholderPoints: 1, 164 | holderType: 'Gold', 165 | delegateType: 'Silver', 166 | holderAmount: 1000, 167 | delegateAmount: 250, 168 | }, 169 | { 170 | User: '0xA602BBA404f3EEA8231398Df0CFA78B46550331d', 171 | holderPoints: 0, 172 | delegatePoints: 5, 173 | recipientsPoints: 1, 174 | badgeholderPoints: 0, 175 | holderType: 'Silver', 176 | delegateType: 'Silver', 177 | holderAmount: 0, 178 | delegateAmount: 2500, 179 | }, 180 | { 181 | User: '0x501EcB2eD1BAFeEDCB122B321618044C07e6C324', 182 | holderPoints: 0, 183 | delegatePoints: 0, 184 | recipientsPoints: 1, 185 | badgeholderPoints: 0, 186 | holderType: null, 187 | delegateType: null, 188 | holderAmount: 0, 189 | delegateAmount: 0, 190 | }, 191 | ]; 192 | -------------------------------------------------------------------------------- /src/utils/badges/type.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for badge data 2 | export type RawSnapshotPoint = { 3 | User: string; 4 | holderPoints: number; 5 | delegatePoints: number; 6 | holderAmount: number; 7 | delegateAmount: number; 8 | recipientsPoints: 0 | 1 | '0' | '1'; 9 | badgeholderPoints: 0 | 1 | '0' | '1'; 10 | holderType: 11 | | 'Bronze' 12 | | 'Diamond' 13 | | 'Platinum' 14 | | 'Gold' 15 | | 'Silver' 16 | | 'Whale' 17 | | null 18 | | 'null'; 19 | delegateType: 20 | | 'Bronze' 21 | | 'Diamond' 22 | | 'Platinum' 23 | | 'Gold' 24 | | 'Silver' 25 | | 'Whale' 26 | | null 27 | | 'null'; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | 3 | export const generateRandomString = ({ 4 | length, 5 | uppercase = false, 6 | lowercase = false, 7 | numerical = false, 8 | special = false, 9 | }: { 10 | length: number; 11 | uppercase?: boolean; 12 | lowercase?: boolean; 13 | numerical?: boolean; 14 | special?: boolean; 15 | }): string => { 16 | if (isNaN(length)) { 17 | throw new TypeError('Length must be a number'); 18 | } 19 | if (length < 1) { 20 | throw new RangeError('Length must be at least 1'); 21 | } 22 | 23 | let result = ''; 24 | const lowerCaseAlphabet = 'abcdefghijklmnopqrstuvwxyz'; 25 | const upperCaseAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 26 | const numberCharacters = '0123456789'; 27 | const specialCharacters = '!@#$%^&*()_-+='; 28 | let characters = ''; 29 | if (uppercase) { 30 | characters += upperCaseAlphabet; 31 | } 32 | if (lowercase) { 33 | characters += lowerCaseAlphabet; 34 | } 35 | if (numerical) { 36 | characters += numberCharacters; 37 | } 38 | if (special) { 39 | characters += specialCharacters; 40 | } 41 | 42 | for (let i = 0; i < length; i++) { 43 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 44 | } 45 | 46 | return result; 47 | }; 48 | 49 | export const validateData = ( 50 | data: string, 51 | hashedData: string, 52 | ): Promise => { 53 | return bcrypt.compare(data, hashedData); 54 | }; 55 | 56 | export const hashData = (data: string): Promise => { 57 | return bcrypt.hash(data, 10); 58 | }; 59 | 60 | export const getPairwiseCombinations = (ids: number[]) => { 61 | const combinations: number[][] = []; 62 | for (let i = 0; i < ids.length; i++) { 63 | for (let j = i + 1; j < ids.length; j++) { 64 | combinations.push([ids[i], ids[j]]); 65 | } 66 | } 67 | return combinations; 68 | }; 69 | 70 | export const sortCombinations = (combinations: number[][], order: number[]) => { 71 | const getScore = (item: number) => order.findIndex((el) => el === item); 72 | 73 | const sorted = [...combinations]; 74 | 75 | sorted.sort((c1, c2) => { 76 | const c1Score = getScore(c1[0]) + getScore(c1[1]); 77 | const c2Score = getScore(c2[0]) + getScore(c2[1]); 78 | 79 | return c1Score - c2Score; 80 | }); 81 | 82 | return sorted; 83 | }; 84 | 85 | export const sortCombinationsByImplicitCategory = ( 86 | combinations: number[][], 87 | getImplicitCat: (id: number) => string, 88 | ) => { 89 | const getScore = (id1: number, id2: number) => 90 | getImplicitCat(id1) === getImplicitCat(id2) ? 1 : -1; 91 | 92 | const sorted = [...combinations]; 93 | 94 | sorted.sort((c1, c2) => { 95 | const c1Score = getScore(c1[0], c1[1]); 96 | const c2Score = getScore(c2[0], c2[1]); 97 | 98 | return c2Score - c1Score; 99 | }); 100 | 101 | return sorted; 102 | }; 103 | 104 | // The highest priority is pairs with the same sub-category 105 | // and least occurance (same sub-category is the first differentiator though) 106 | export const sortCombinationsByImplicitCategoryAndOccurance = ( 107 | combinations: number[][], 108 | getProjectOccurances: (id: number) => number, 109 | getImplicitCat: (id: number) => string, 110 | getImplicitCatScore: (cat: string) => number, 111 | ) => { 112 | const compareImplicitCat = (id1: number, id2: number) => { 113 | if (getProjectOccurances(id1) + getProjectOccurances(id2) > 2) { 114 | return getImplicitCat(id1) === getImplicitCat(id2) ? -10 : 10; 115 | } 116 | return getImplicitCat(id1) === getImplicitCat(id2) ? 10 : -10; 117 | }; 118 | 119 | const calculateProjectOccuranceScore = (id: number) => { 120 | const occurance = getProjectOccurances(id); 121 | 122 | if (occurance > 1) return occurance * 3; 123 | return occurance; 124 | }; 125 | 126 | const sorted = [...combinations]; 127 | 128 | sorted.sort((c1, c2) => { 129 | const c1Score = 130 | getImplicitCatScore(getImplicitCat(c1[0])) + 131 | getImplicitCatScore(getImplicitCat(c1[1])) + 132 | compareImplicitCat(c1[0], c1[1]) - 133 | calculateProjectOccuranceScore(c1[0]) - 134 | calculateProjectOccuranceScore(c1[1]); 135 | const c2Score = 136 | getImplicitCatScore(getImplicitCat(c2[0])) + 137 | getImplicitCatScore(getImplicitCat(c2[1])) + 138 | compareImplicitCat(c2[0], c2[1]) - 139 | calculateProjectOccuranceScore(c2[0]) - 140 | calculateProjectOccuranceScore(c2[1]); 141 | 142 | console.log('score of', c1, '=', c1Score); 143 | console.log('score of', c2, '=', c2Score); 144 | return c2Score - c1Score; 145 | }); 146 | 147 | return sorted; 148 | }; 149 | 150 | // Seeded random number generator 151 | class SeededRandom { 152 | private seed: number; 153 | 154 | constructor(seed: number) { 155 | this.seed = seed; 156 | } 157 | 158 | // Generate a random number between 0 and 1 159 | random(): number { 160 | const x = Math.sin(this.seed++) * 10000; 161 | return x - Math.floor(x); 162 | } 163 | } 164 | 165 | // Fisher-Yates shuffle algorithm 166 | export function shuffleArraySeeded(array: T[], seed: number): T[] { 167 | const shuffled = [...array]; 168 | const random = new SeededRandom(seed); 169 | 170 | for (let i = shuffled.length - 1; i > 0; i--) { 171 | const j = Math.floor(random.random() * (i + 1)); 172 | [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 173 | } 174 | 175 | return shuffled; 176 | } 177 | 178 | export const sortProjectId = ( 179 | project1Id: number, 180 | project2Id: number, 181 | ): [number, number] => { 182 | return project1Id > project2Id 183 | ? [project2Id, project1Id] 184 | : [project1Id, project2Id]; 185 | }; 186 | 187 | export const STAGING_API = 'pairwise.cupofjoy.store'; 188 | 189 | export function areEqualNumberArrays( 190 | array1: number[], 191 | array2: number[], 192 | // arr3: number[], 193 | ): boolean { 194 | const arr1 = [...array1]; 195 | const arr2 = [...array2]; 196 | const N = arr1.length; 197 | const M = arr2.length; 198 | // const O = arr3.length; 199 | 200 | // If lengths of array are not equal means array are not equal 201 | if (N !== M /* || N !== O || M !== O */) return false; 202 | 203 | // Sort both arrays 204 | arr1.sort((a, b) => a - b); 205 | arr2.sort((a, b) => a - b); 206 | // arr3.sort((a, b) => a - b); 207 | 208 | // Linearly compare elements 209 | for (let i = 0; i < N; i++) 210 | if (arr1[i] !== arr2[i] /* || arr1[i] !== arr3[i] || arr2[i] !== arr3[i]*/) 211 | return false; 212 | 213 | // If all elements were same. 214 | return true; 215 | } 216 | -------------------------------------------------------------------------------- /src/utils/mathematical-logic/index.ts: -------------------------------------------------------------------------------- 1 | import Matrix, { EigenvalueDecomposition } from 'ml-matrix'; 2 | 3 | export const generateZeroMatrix = (n: number): number[][] => { 4 | const matrix: number[][] = []; 5 | for (let i = 0; i < n; i++) { 6 | matrix[i] = []; 7 | for (let j = 0; j < n; j++) { 8 | matrix[i][j] = 0; 9 | } 10 | } 11 | return matrix; 12 | }; 13 | 14 | const validateVotesMatrix = (votesMatrix: number[][]) => { 15 | for (let i = 0; i < votesMatrix.length; i++) { 16 | for (let j = 0; j < votesMatrix[i].length; j++) { 17 | const cell = votesMatrix[i][j]; 18 | // values should be either 0 or 1 19 | if (![0, 1].includes(cell)) 20 | throw Error('Invalid value for a pairwise vote'); 21 | // diagnoals should be zero 22 | if (i === j) { 23 | if (cell !== 0) 24 | throw new Error("You can't compare a project with itself"); 25 | } 26 | // You can't prefer project A to B and then again B to A 27 | if (cell + votesMatrix[j][i] > 1) 28 | throw new Error(`Invalid value at ${i},${j} and ${j},${i}`); 29 | } 30 | if (votesMatrix[i].length !== votesMatrix.length) 31 | throw new Error('Matrix is not square'); 32 | } 33 | return true; 34 | }; 35 | 36 | export const toFixedNumber = (num: number, digits: number) => { 37 | const pow = Math.pow(10, digits); 38 | return Math.round(num * pow) / pow; 39 | }; 40 | 41 | const isRankingUseful = (ranking: number[]) => { 42 | const numOfZeros = ranking.filter( 43 | (score) => toFixedNumber(score, 3) <= 0.001, 44 | ).length; 45 | 46 | if (numOfZeros > Math.round(ranking.length / 10)) return false; 47 | 48 | const sortedRanking = [...ranking].sort(); 49 | 50 | let median = sortedRanking[Math.floor(sortedRanking.length / 2)]; 51 | if (sortedRanking.length % 2 === 0) { 52 | median = 53 | (median + sortedRanking[Math.floor(sortedRanking.length / 2) - 1]) / 2; 54 | } 55 | const max = sortedRanking[sortedRanking.length - 1]; 56 | 57 | if (toFixedNumber(median, 3) <= 0.001) return false; 58 | 59 | if (max / median > 10) return false; 60 | 61 | return true; 62 | }; 63 | 64 | function cloneArray(a: T): T { 65 | const array = a.map((e) => (Array.isArray(e) ? cloneArray(e) : e)) as T; 66 | 67 | return array; 68 | } 69 | 70 | export const getRankingForSetOfDampingFactors = (input: number[][]) => { 71 | // const dampingFactors = [0.85]; 72 | const dampingFactors = [ 73 | 1, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35, 74 | 0.3, 0.25, 0.2, 0.15, 0.1, 0.05, 0, 75 | ]; 76 | // const dampingFactors = [ 77 | // 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 78 | // 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1, 79 | // ]; 80 | 81 | let isUseful = false; 82 | let i = 0; 83 | let ranking: number[] = []; 84 | while (!isUseful && i < dampingFactors.length) { 85 | try { 86 | ranking = calculateCollectionRanking(input, dampingFactors[i]); 87 | isUseful = isRankingUseful(ranking); 88 | } catch (e) { 89 | console.error(e); 90 | } finally { 91 | i += 1; 92 | } 93 | } 94 | 95 | if (!ranking) { 96 | console.error('No useful ranking available for this vote matrix'); 97 | } 98 | 99 | return ranking; 100 | }; 101 | 102 | const validate = (input: { share: number }[]) => { 103 | const sum = input.reduce((acc, curr) => (acc += curr.share), 0); 104 | 105 | if (sum === 1) return true; 106 | 107 | return false; 108 | }; 109 | 110 | // export function makeIt100(input: T[]) { 111 | // let result = [...input]; 112 | 113 | // let breakLimit = 0; 114 | // while (!validate(result) && breakLimit < 100) { 115 | // const sum = result.reduce((acc, curr) => (acc += curr.share), 0); 116 | 117 | // const temp = result.map((item) => ({ 118 | // ...item, 119 | // share: (item.share * 1) / sum, 120 | // })); 121 | 122 | // result = temp.map((item) => ({ 123 | // ...item, 124 | // share: toFixedNumber(item.share, 6), 125 | // })); 126 | 127 | // breakLimit++; 128 | // } 129 | 130 | // return result; 131 | // } 132 | 133 | export const calculateCollectionRanking = ( 134 | input: number[][], 135 | dampingFactor = 1, 136 | ) => { 137 | let votesMatrix: any = cloneArray(input); 138 | 139 | validateVotesMatrix(votesMatrix); 140 | 141 | const length = votesMatrix.length; 142 | 143 | // Set the diagnoals to the sum of the rows 144 | 145 | for (let i = 0; i < length; i++) { 146 | let sum = 0; 147 | for (let j = 0; j < length; j++) { 148 | const cell = votesMatrix[i][j]; 149 | sum += cell; 150 | } 151 | votesMatrix[i][i] = sum; 152 | } 153 | 154 | // Divide each column's items by the sum of the column's items 155 | 156 | for (let i = 0; i < length; i++) { 157 | let sum = 0; 158 | for (let j = 0; j < length; j++) { 159 | const cell = votesMatrix[j][i]; 160 | sum += cell; 161 | } 162 | for (let j = 0; j < length; j++) { 163 | if (sum !== 0) votesMatrix[j][i] = votesMatrix[j][i] / sum; 164 | else votesMatrix[j][i] = 0; 165 | } 166 | } 167 | 168 | votesMatrix = new Matrix(votesMatrix); 169 | 170 | // add a damping factor 171 | const dampingMatrix = new Matrix( 172 | Array(length).fill(Array(length).fill((1 - dampingFactor) / length)), 173 | ); 174 | 175 | votesMatrix = dampingMatrix.add(votesMatrix.mul(dampingFactor)); 176 | 177 | const e = new EigenvalueDecomposition(votesMatrix); 178 | const values = e.realEigenvalues; 179 | // const imaginary = e.imaginaryEigenvalues; 180 | const vectors = e.eigenvectorMatrix; 181 | 182 | const index = findEigenvalueOfOne(values); 183 | 184 | const filtered = vectors.getColumn(index); 185 | 186 | // Divide by the smallest component 187 | return divideBySum(divideBySmallest(filtered)); 188 | }; 189 | 190 | const findEigenvalueOfOne = (eigenvalues: number[]) => 191 | eigenvalues.findIndex( 192 | (item) => Math.abs(toFixedNumber(1 - toFixedNumber(item, 3), 3)) <= 0.001, 193 | ); 194 | 195 | const divideBySmallest = (numbers: number[]): number[] => { 196 | let min = Math.abs(numbers[0]); 197 | 198 | for (let i = 1; i < numbers.length; i++) { 199 | min = Math.min(min, Math.abs(numbers[i])); 200 | } 201 | 202 | const result: number[] = []; 203 | 204 | if (min === 0) return numbers; 205 | 206 | for (const num of numbers) { 207 | result.push(num / min); 208 | } 209 | 210 | return result; 211 | }; 212 | 213 | const divideBySum = (numbers: number[]) => { 214 | const sum = numbers.reduce((acc, curr) => (acc += curr), 0); 215 | 216 | return numbers.map((item) => toFixedNumber(item / sum, 4)); 217 | }; 218 | -------------------------------------------------------------------------------- /src/utils/types/AuthedReq.type.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export interface AuthedReq extends Request { 4 | userId: number; 5 | } 6 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "strict": true, 14 | "strictPropertyInitialization": false, 15 | "incremental": true, 16 | "skipLibCheck": true, 17 | "noFallthroughCasesInSwitch": false 18 | } 19 | } 20 | --------------------------------------------------------------------------------