├── .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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------