Create app

This commit is contained in:
Mikko
2021-09-27 03:06:54 +03:00
parent 11a7f859b3
commit 2dbed597e0
23 changed files with 4201 additions and 15 deletions
+1
View File
@@ -0,0 +1 @@
MONGODB = mongodb://localhost:27017/rarity-analyser
+5
View File
@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
dist
src/data/
+11
View File
@@ -0,0 +1,11 @@
/* eslint-disable no-undef */
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
}
+5 -1
View File
@@ -1,5 +1,9 @@
.DS_Store
node_modules
# Keep environment variables out of version control
.env
src/data/
src/data/*
dist
+2
View File
@@ -0,0 +1,2 @@
dist
src/data/*
+1
View File
@@ -0,0 +1 @@
web: yarn start
+125
View File
@@ -0,0 +1,125 @@
# Rarity analyser
Creates rarity data for an NFT collection and provides a GraphQL API for making queries.
Creates both classic and normalized rarity scores.
User interface at [rarity-interface](https://github.com/mikko-o/rarity-interface).
## Getting started
### Create a MongoDB instance
1. Create a free [MongoDB Atlas instance](https://www.mongodb.com/) or use a [local MongoDB](https://docs.mongodb.com/manual/administration/install-community/).
2. Add the [connection string](https://docs.mongodb.com/manual/reference/connection-string/) for MongoDB into a .env file:
```
# .env
MONGODB=mongodb://localhost:27017/rarity
```
### Analyse metadata
Analyse collection [metadata](https://docs.opensea.io/docs/metadata-standards) with
`yarn analyse --json <path_to_metadata_json_file>`.
### Start the GraphQL server
Start the server with `yarn dev`
### Deployment
#### Heroku
```
heroku create
heroku config:set MONGODB=mongodb+srv://<user>:<password>@db.b5lu1.mongodb.net/db
git push heroku main
```
## Configuration
Following options can be applied in the rarityConfig.ts file.
### weights
Add weights to scale rarity scores up or down.
```typescript
# rarityConfig.ts
{
weights: {
"Trait A": 1.2,
"Trait B": 0,
}
}
```
### getMissingTraitIdentifier
`(trait_type?: string) => string | undefined`
By default it is assumed that the missing traits are completely missing from the attribute list. Some collections however use other values for missing traits, for example "No Hat" for the trait type "Hat".
Example:
```typescript
# rarityConfig.ts
{
getMissingTraitIdentifier: (traittype?: string) => "No " + traitType
}
```
### attributesFieldName
The default name is `attributes`, define a custom name here.
## API
### analyse
`yarn analyse --json <collection.json> --saveJson --db <true>`
Analyses rarity data form existing metadata
Cli options:
#### --json
Default: `src/data/collection.json`
Path to the token attribute file
#### --saveJson
Default: `false`
Saves rarity data into a json file
#### --db
Default: `true`
Skips saving to database if false
### Interfaces
##### Token
```typescript
interface Token {
id: number
name: string
attributes: Attribute[] // Attribute field name can be configured in rarityConfig.ts
image?: string
description?: string
image_data?: string
external_url?: string
animation_url?: string
background_color?: string
// The following fields are included in the analysed data
rarity_score: number
rank: number
rarity_score_normalized: number
rank_normalized: number
}
```
##### Attribute
```typescript
interface Attribute {
trait_type: number
value: string
// The following fields are included in the analysed data
rarity_score: number
rarity_score_normalized: number
percentile: number
count: number // Count of how many tokens have the value of this attribute
}
```
+19 -10
View File
@@ -1,31 +1,40 @@
{
"name": "rarity-graphql",
"version": "1.0.0",
"main": "index.js",
"main": "src/index.ts",
"author": "mikkoo.eth",
"license": "MIT",
"private": false,
"scripts": {
"build": "yarn tsc",
"start": "node dist/index.js",
"dev": "yarn ts-node src/index.ts",
"start": "node dist/src/index.js",
"dev": "yarn ts-node-dev src/index.ts",
"analyse": "yarn ts-node src/scripts/analyse.ts",
"seed": "yarn ts-node src/scripts/seed.ts"
"seed": "yarn ts-node src/scripts/seed.ts",
"lint": "yarn eslint . --ext .js,.ts",
"format": "yarn prettier --write . --no-semi --single-quote"
},
"dependencies": {
"@graphql-tools/schema": "^8.2.0",
"apollo-server": "^3.3.0",
"axios": "^0.21.4",
"dotenv": "^10.0.0",
"graphql": "^15.5.3",
"mongoose": "^6.0.5"
"lodash.orderby": "^4.6.0",
"micro": "^9.3.4",
"mongoose": "^6.0.5",
"typescript": "^4.4.3",
"yargs": "^17.1.1-candidate.0"
},
"devDependencies": {
"@types/lodash.orderby": "^4.6.6",
"@types/node": "^16.7.13",
"@types/yargs": "^17.0.2",
"lodash.orderby": "^4.6.0",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"prettier": "^2.4.1",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8",
"typescript": "^4.4.2",
"yargs": "^17.1.1-candidate.0"
"ts-node-dev": "^1.1.8"
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Config } from './src/types'
const config: Config = {
// weights: {},
// getMissingTraitIdentifier: (trait_type?: string) => undefined,
// attributesFieldName: 'attributes',
}
export default config
+515
View File
@@ -0,0 +1,515 @@
import { orderBy } from 'lodash'
import {
Token,
TokenWithRarity,
Collection,
IsMissing,
GetIsMissing,
GetMissingTraitIdentifier,
Weights,
Attribute,
TraitsMeta,
} from './types'
import config from '../rarityConfig'
import { defaultMissingTraitIdentifier } from './utils'
const getMissingTraitIdentifier =
config.getMissingTraitIdentifier ?? defaultMissingTraitIdentifier
const weights: Weights = config.weights ?? {}
const getWeight = (traitType: string) => weights[traitType] ?? 1
const MISSING = 'none'
/**
* @param missingTraitIdentifier Value of the trait type that indicates that it is missing. `undefined` (default) if there is no specific identifier and the {trait_type: string, value: string} entry is simply omitted.
* @returns A function, which given traitType and token returns true if the trait type is missing (as specified by missingTraitIdentifier). Returns true also if the given traitType is completely missing from the tokens attributes
*/
const getIsMissing: GetIsMissing =
(getMissingTraitIdentifier) => (traitType, token) => {
const attribute = token.attributes.find(
(att) => att.trait_type === traitType
)
return (
!attribute || attribute.value === getMissingTraitIdentifier(traitType)
)
}
const traitIsMissingFn = getIsMissing(getMissingTraitIdentifier)
type Counts = {
[trait_type: string]: {
[value: string]: number
}
}
/**
* @returns Collection with rarity data
*/
export const analyse = (
t: Array<Record<string, string | number | Attribute[]>>
): Collection => {
const tokens: Token[] = preprocess(t)
const n = tokens.length
let traitTypes = getTraitTypeSet(tokens)
const tokensWithMissingTraits = addMissingTraits(
tokens,
traitIsMissingFn,
traitTypes,
getMissingTraitIdentifier
)
const tokensWithTraitCount = addTraitCounts(
tokensWithMissingTraits,
getMissingTraitIdentifier
)
traitTypes = getTraitTypeSet(tokensWithTraitCount)
const counts = count(tokensWithTraitCount)
const tokensWithAttributeRarity = addAttributeRarities(
tokensWithTraitCount,
counts
)
// Sort attributes by rarity_score
const tokensWithOrderedAttributes = tokensWithAttributeRarity.map(
(token) => ({
...token,
attributes: orderBy(token.attributes, (a) => a.rarity_score, 'desc'),
})
)
const tokensWithTotalRarityScore = getTotalRarityScore(
tokensWithOrderedAttributes
)
const tokensWithRankings = getRankings(tokensWithTotalRarityScore)
const traitTypeData = getTraitRarityScores(tokensWithRankings, [...traitTypes])
const [traitTypeDataWithStats, collectionStats] = getStats(traitTypeData)
const traitTypeDataWithNormalizedRarities = addNormalizedRarities(traitTypeDataWithStats, collectionStats.mean)
const tokensWithNormalizedRarities = addTokenNormalizedRarities(tokensWithRankings, traitTypeDataWithNormalizedRarities)
const tokensWithTotalNormalizedRarity = getTotalNormalizedRarityScore(tokensWithNormalizedRarities)
const tokensWithNormalizedRankings = getNormalizedRankings(tokensWithTotalNormalizedRarity)
return {
tokens: tokensWithNormalizedRankings,
numTokens: n,
traitTypes: traitTypeDataWithNormalizedRarities,
stats: collectionStats,
}
}
const preprocess = (
tokens: Record<string, string | number | Attribute[]>[]
) => {
const attributesFieldName = config.attributesFieldName
// Filter possible empty objects, ie. trait_type and value are missing
const filtered: Array<
Record<string, string | number | Attribute[]> & {
attributes: Attribute[]
}
> = tokens.map((token) => {
const attributes = token[
attributesFieldName ?? 'attributes'
] as Attribute[]
if (!('trait_type' in attributes[0]))
throw new Error('Attribute trait type missing')
const filteredAttributes = attributes.filter(
(a) => Object.keys(a).length > 0
)
return { ...token, attributes: filteredAttributes }
})
let t = filtered
// If attribute field name is not 'attributes', rename the field
if (attributesFieldName && attributesFieldName !== 'attributes') {
const mapped = filtered.map((token) => {
const attributes = token[attributesFieldName] as Attribute[]
if (!('trait_type' in attributes[0]))
throw new Error('Attribute trait_type missing')
const newToken: Record<string, string | number | Attribute[]> & {
attributes: Attribute[]
} = {
...token,
attributes: attributes,
}
delete newToken[attributesFieldName]
return newToken
})
t = mapped
}
return t as Token[]
}
/**
* @param tokens
* @param traitIsMissingFn Function that given a token and a trait type returns true if the trait type is missing
* @param traitTypes A set of trait types
* @param missingTraitIdentifier Value of a trait type that is missing or not active on the token. By default (undefined) it is assumed that the trait type is completely missing from the attribute list.
* @returns
*/
const addMissingTraits = (
tokens: Token[],
traitIsMissingFn: IsMissing,
traitTypes: Set<string>,
getMissingTraitIdentifier: GetMissingTraitIdentifier
) =>
tokens.map((token) => {
const missingAttributes = getMissingTraits(
token,
traitIsMissingFn,
traitTypes,
getMissingTraitIdentifier
)
// Delete old missing attributes if exists
const missingTraits = new Set(missingAttributes.map((m) => m.trait_type))
const filteredAttributes = token.attributes.filter(
(a) => !missingTraits.has(a.trait_type)
)
return {
...token,
attributes: [...filteredAttributes, ...missingAttributes],
}
})
/**
* @returns Set of a tokens missing traits as attributes with placeholder rarity data
*/
const getMissingTraits = <T extends Token>(
token: T,
isMissingFn: IsMissing,
traitTypes: Set<string>,
getMissingTraitIdentifier: GetMissingTraitIdentifier
) =>
[...traitTypes]
.filter((traitType) => isMissingFn(traitType, token))
.map((trait_type) => ({
trait_type,
value: getMissingTraitIdentifier(trait_type) ?? MISSING,
rarity_score: 0,
count: 0,
percentile: 0,
}))
/**
* @returns tokens with trait_count attribute (`{trait_type: 'trait_count', value: number}`)
*/
const addTraitCounts = (
tokens: Token[],
getMissingTraitIdentifier: GetMissingTraitIdentifier
) =>
tokens.map((token) => {
const count = token.attributes.filter(
(a) => a.value !== getMissingTraitIdentifier(a.trait_type) ?? MISSING
).length
const traitCountAttribute = {
trait_type: 'Trait Count',
value: String(count),
rarity_score: 0,
percentile: 0,
count: 0,
}
return {
...token,
attributes: [...token.attributes, traitCountAttribute],
}
})
/**
* @returns counts of how many times each different attribute value occurs in the collection
*/
const count = (tokens: Token[]): Counts => {
const counts: Counts = {}
tokens.forEach((token) => {
token.attributes.forEach(({ trait_type, value }) => {
const count = counts[trait_type] ? counts[trait_type][value] ?? 0 : 0
if (counts[trait_type]) counts[trait_type][value] = count + 1
else counts[trait_type] = { [value]: 1 }
})
})
return counts
}
/**
* @returns tokens with rarity_score, percentile. Include count (how many tokens have this value)
*/
const addAttributeRarities = (tokens: Token[], counts: Counts) =>
tokens.map((token) => {
const updatedAttributes = token.attributes.map((attribute) => {
const { trait_type, value } = attribute
const count = counts[trait_type][value]
const percentile = count / tokens.length
const rarity_score = (1 / percentile) * getWeight(trait_type)
return { ...attribute, rarity_score, percentile, count }
})
return { ...token, attributes: updatedAttributes }
})
/**
* @returns set of different trait types in the collection
*/
const getTraitTypeSet = (tokens: Token[]): Set<string> => {
const traitTypes = new Set<string>()
tokens.forEach((token) => {
token.attributes.forEach(({ trait_type }) => {
if (!traitTypes.has(trait_type)) traitTypes.add(trait_type)
})
})
return traitTypes
}
const getTotalRarityScore = (
tokens: Token[]
) => {
const totalRarityScores = tokens.map((token) => {
const rarity_score = token.attributes.reduce(
(sum, attribute) => sum + attribute.rarity_score,
0
)
return { ...token, rarity_score }
})
return totalRarityScores
}
const getTotalNormalizedRarityScore = (
tokens: Omit<TokenWithRarity, 'rarity_score_normalized' | 'rank_normalized'>[]
) => {
const totalRarityScores = tokens.map((token) => {
const rarity_score_normalized = token.attributes.reduce((sum, attribute) => {
if (attribute.rarity_score_normalized === undefined) throw new Error("Normalized rarity score missing")
return sum + attribute.rarity_score_normalized
}, 0
)
return { ...token, rarity_score_normalized }
})
return totalRarityScores
}
/**
* @param tokens Array of tokens sorted by rarity_score (desc)
* @returns Tokens with rankins
*/
const getRankings = (tokens: Omit<TokenWithRarity, 'rank' | 'rarity_score_normalized' | 'rank_normalized'>[]) => {
const sorted = orderBy(tokens, (token) => token.rarity_score, 'desc')
let currentRank = 0
let previousRarityScore = Infinity
const rankedTokens = sorted.map((token, i) => {
if (token.rarity_score < previousRarityScore) {
currentRank = i + 1
}
const updated: Omit<TokenWithRarity, 'rarity_score_normalized' | 'rank_normalized'> = { ...token, rank: currentRank }
previousRarityScore = updated.rarity_score
return updated
})
return rankedTokens
}
/**
* @param tokens Array of tokens sorted by rarity_score (desc)
* @returns Tokens with rankins
*/
const getNormalizedRankings = (tokens: Omit<TokenWithRarity, 'rank_normalized'>[]) => {
const sorted = orderBy(tokens, (token) => token.rarity_score_normalized, 'desc')
let currentRank = 0
let previous = Infinity
const rankedTokens = sorted.map((token, i) => {
if (token.rarity_score_normalized < previous) {
currentRank = i + 1
}
const updated = { ...token, rank_normalized: currentRank }
previous = updated.rarity_score_normalized
return updated
})
return rankedTokens
}
const getTraitRarityScores = (tokens: Token[], traitTypes: string[]) => {
const temp: Record<string, Record<string, Attribute>> = {}
traitTypes.forEach((t) => (temp[t] = {}))
tokens.forEach((token) => {
token.attributes.forEach((attribute) => {
const { trait_type, rarity_score, value } = attribute
const attributes = temp[trait_type]
if (!attributes) return
const current = attributes[value]
if (!current || rarity_score > current.rarity_score) {
temp[trait_type][value] = attribute
}
})
})
const result = Object.entries(temp).map(([key, val]) => ({
trait_type: key,
attributes: orderBy(
Object.entries(val).map((val) => val[1]),
x => x.count
),
}))
return result
}
const getStats = (
traitRarities: { trait_type: string; attributes: Attribute[] }[]
) => {
const dataWithStats = traitRarities.map((trait) => {
const [sum, min, max] = trait.attributes
.map((a) => a.rarity_score)
.reduce(
([sum, min, max], rarity_score) => [
sum + rarity_score,
Math.min(min, rarity_score),
Math.max(max, rarity_score),
],
[0, Number.MAX_VALUE, 0]
)
const mean = sum / trait.attributes.length
const temp = trait.attributes
.map((a) => a.rarity_score)
.reduce((acc, rarity) => acc + Math.pow(rarity - mean, 2))
const std = Math.sqrt(temp / trait.attributes.length)
return {
...trait,
stats: { mean, std, min, max },
numValues: trait.attributes.length,
}
})
const [sum, min, max] = dataWithStats.reduce(
([sum, min, max], trait) => {
const s = trait.attributes.reduce(
(sum, attribute) => sum + attribute.rarity_score,
0
)
return [s, Math.min(min, trait.stats.min), Math.max(max, trait.stats.max)]
},
[0, Number.MAX_VALUE, 0]
)
const mean = sum / dataWithStats.length
const temp = dataWithStats.reduce(
(acc, rarity) =>
acc +
rarity.attributes.reduce(
(sum, a) => sum + Math.pow(a.rarity_score - mean, 2),
0
),
0
)
const std = Math.sqrt(
temp /
dataWithStats.reduce((count, trait) => count + trait.attributes.length, 0)
)
return [dataWithStats, { mean, std, min, max }] as const
}
/**
* The idea with normalization is the following:
* If a trait type has more possible values than an average type,it will generally get higher rarity scores only for that reason.
* Example:
* If four values are equally distributed among 800 tokens, this trait will be add a rarity score of 4 to the token's total rarity.
* If there are 5 values, each trait will add 5.
* Therefore the trait types with more values need to be scaled down.
* However some traits types include rarer values than others
* We will take into account by checking whether the average rarity of the trait type
* is higher than the average rarity of the whole collection.
* Example:
* Trait has 30% more traits than the average, but 40% higher average rarity
* -> 10% of the higher rarity is due to actual rarity and scaling down 30% would remove some of that
* Solution is to 1. remove and store 10%, 2. remove 30%, 3. add 10% back.
*/
const addNormalizedRarities = (
traitTypes: TraitsMeta[],
meanRarity: number
) => {
const tempSum = traitTypes.reduce((sum, type) => sum + type.numValues, 0)
const meanValueCount = tempSum / traitTypes.length
const attributesWithNormalizedRarities = traitTypes.map(
metaTrait => {
const { stats, numValues, attributes } = metaTrait
let a: number
if (stats.mean >= meanRarity)
a = (stats.mean - meanRarity) / stats.mean
else
a = (meanRarity - stats.mean) / meanRarity
let b: number
if (numValues >= meanValueCount)
b = (numValues - meanValueCount) / numValues
else
b = (meanValueCount - numValues) / meanValueCount
const c = numValues >= meanValueCount ? 1 - b : 1 + b
const updated = attributes.map(attribute => {
const r = attribute.rarity_score
let rarity_score_normalized
if (a >= b && ((stats.mean > meanRarity && numValues > meanValueCount) || (stats.mean < meanRarity && numValues < meanValueCount)))
rarity_score_normalized = (r - (a - b) * r) * c + (a - b) * r
else
rarity_score_normalized = (r - a * r) * c + a * r
return {
...attribute,
rarity_score_normalized
}
})
return {
...metaTrait,
attributes: updated
}
}
)
return attributesWithNormalizedRarities
}
const addTokenNormalizedRarities = (tokens: Omit<TokenWithRarity, 'rarity_score_normalized' | 'rank_normalized'>[], traitTypes: TraitsMeta[]) => {
const normalizedMap = {} as { [traitType: string]: { [value: string]: number } }
traitTypes.forEach(type => {
type.attributes.forEach(({ trait_type, value, rarity_score_normalized }) => {
if (rarity_score_normalized === undefined) throw new Error('Normalized rarity score missing')
if (!normalizedMap[trait_type])
normalizedMap[trait_type] = {}
normalizedMap[trait_type][value] = rarity_score_normalized
})
})
return tokens.map(token => ({
...token,
attributes: token.attributes.map(attribute => ({
...attribute,
rarity_score_normalized: normalizedMap[attribute.trait_type][attribute.value]
}))
}))
}
+12
View File
@@ -0,0 +1,12 @@
import mongoose from 'mongoose'
import dotenv from 'dotenv'
dotenv.config()
const connect = (uri?: string) => {
const s = uri ?? process.env.MONGODB
if (!s) throw new Error('Connection URI not found')
mongoose.connect(s)
}
export default connect
+6
View File
@@ -0,0 +1,6 @@
import mongoose from 'mongoose'
import { metaSchema, tokenSchema } from './schema'
export const Token = mongoose.model('Token', tokenSchema)
export const Meta = mongoose.model('Meta', metaSchema)
+121
View File
@@ -0,0 +1,121 @@
import mongoose from 'mongoose'
import {
AttributeWithRarity,
Meta,
Stats,
TokenWithRarity,
TraitsMeta,
} from '../types'
const { Schema } = mongoose
const attributeSchema = new Schema<AttributeWithRarity>({
trait_type: {
type: String,
required: true,
index: true,
},
value: {
type: String,
required: true,
index: true,
},
rarity_score: {
type: Number,
required: true,
index: true,
},
rarity_score_normalized: {
type: Number,
required: true,
index: true,
},
percentile: {
type: Number,
required: true,
},
count: {
type: Number,
required: true,
},
})
export const tokenSchema = new Schema<TokenWithRarity>({
id: {
type: Number,
required: true,
unique: true,
index: true,
},
name: String,
attributes: [attributeSchema],
description: String,
image: String,
image_data: String,
external_url: String,
animation_url: String,
background_color: String,
rank: {
type: Number,
index: true,
},
rarity_score: {
type: Number,
index: true,
},
rarity_score_normalized: {
type: Number,
index: true,
},
rank_normalized: {
type: Number,
index: true,
},
})
const statsSchema = new Schema<Stats>({
mean: {
type: Number,
required: true,
},
std: {
type: Number,
required: true,
},
min: {
type: Number,
required: true,
},
max: {
type: Number,
required: true,
},
})
const traitTypeMetaSchema = new Schema<TraitsMeta>({
trait_type: {
type: String,
required: true,
},
attributes: {
type: [attributeSchema],
required: true,
},
stats: statsSchema,
numValues: {
type: Number,
required: true,
},
})
export const metaSchema = new Schema<Meta>({
numTokens: {
type: Number,
required: true,
},
traitTypes: {
type: [traitTypeMetaSchema],
required: true,
},
stats: statsSchema,
})
+21
View File
@@ -0,0 +1,21 @@
import mongoose from 'mongoose'
import { Token, Meta } from './models'
import { Collection } from '../types'
import connect from './connection'
const seed = async (collection: Collection) => {
connect()
await Promise.all([Token.deleteMany({}), Meta.deleteMany({})])
const { tokens, ...metadata } = collection
await Promise.all([
Token.insertMany(tokens).catch(console.error),
Meta.create(metadata),
])
mongoose.disconnect()
}
export default seed
+56
View File
@@ -0,0 +1,56 @@
import { Token, Meta } from '../db/models'
interface TokensArgs {
first: number
skip: number
orderBy: string
orderDirection: string
trait_count: number
traits: Array<{
trait_type: string
value: string
}>
id: number
}
const resolvers = {
Query: {
tokens(parent: any, args: TokensArgs) {
const { first, skip, orderBy, orderDirection, traits, id } = args
const [trait_types, values] = traits
? traits.reduce(
([trait_types, values], trait) => [
[...trait_types, trait.trait_type],
[...values, trait.value],
],
[[] as string[], [] as string[]]
)
: []
return Token.find({
...(id ? { id } : {}),
...(traits
? {
'attributes.trait_type': { $in: trait_types },
'attributes.value': { $in: values },
}
: {}),
})
.sort({ [orderBy ?? 'rarity_score']: orderDirection ?? 'desc' })
.skip(skip)
.limit(first)
.catch(console.log)
},
token(parent: any, args: { id?: number; rank?: number }) {
const { id, rank } = args
return Token.findOne({
...(rank ? { rank } : {}),
...(id ? { id } : {}),
}).catch(console.log)
},
meta: () => Meta.findOne({}).catch(console.log),
},
}
export default resolvers
+82
View File
@@ -0,0 +1,82 @@
import { gql } from 'apollo-server-core'
const typeDefs = gql`
type Attribute {
id: Int!
trait_type: String!
value: String
rarity_score: Float!
rarity_score_normalized: Float!
percentile: Float!
count: Float!
tokenId: Int!
}
type TraitCount {
count: Int!
percentile: Float!
rarity_score: Float!
tokenId: Int!
}
type Token {
id: Int!
name: String!
attributes: [Attribute!]!
metadataUrl: String!
trait_count: TraitCount!
description: String
image: String
image_data: String
external_url: String
animation_url: String
background_color: String
rank: Float!
rarity_score: Float!
rarity_score_normalized: Float!
rank_normalized: Float!
}
type AttributeMeta {
trait_type: String!
rarity_score: Float!
rarity_score_normalized: Float!
value: String
percentile: Float!
count: Float!
}
type Stats {
mean: Float!
std: Float!
min: Float!
max: Float!
}
type TraitTypeMeta {
trait_type: String!
attributes: [AttributeMeta!]!
stats: Stats!
numValues: Int!
}
type Meta {
numTokens: Int!
traitTypes: [TraitTypeMeta!]!
stats: Stats!
}
input Trait {
trait_type: String
value: String
}
type Query {
tokens(
first: Int
skip: Int
orderBy: String
orderDirection: String
traits: [Trait]
id: Int
): [Token]
token(id: Int, rank: Int): Token
meta: Meta
}
`
export default typeDefs
+24
View File
@@ -0,0 +1,24 @@
import { ApolloServer } from 'apollo-server'
import connect from './db/connection'
import typeDefs from './graphql/typeDefs'
import resolvers from './graphql/resolvers'
import {
ApolloServerPluginLandingPageGraphQLPlayground
} from "apollo-server-core";
import dotenv from 'dotenv'
dotenv.config()
connect()
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground
],
})
server.listen({ port: process.env.PORT ?? 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
+46
View File
@@ -0,0 +1,46 @@
import { readFileSync, writeFileSync } from 'fs'
import yargs from 'yargs'
import { analyse } from '../analysis'
import seed from '../db/seed'
import { Token } from '../types'
const argv = yargs(process.argv.slice(2)).options({
json: {
type: 'string',
default: 'src/data/collection.json',
describe: 'Path to the token attribute file',
},
saveJson: {
type: 'boolean',
default: false,
describe: 'Use this flag to save data into a json file',
},
db: {
type: 'boolean',
default: true,
describe: 'If true, adds data to database',
}
}).argv
if ('json' in argv) {
console.log('Analysing data...')
const rawData = readFileSync(argv.json, { encoding: 'utf-8' })
const tokens: Token[] = JSON.parse(rawData)
const collection = analyse(tokens)
if (argv.saveJson) {
writeFileSync(
'src/data/' + 'collection-rarities.json',
JSON.stringify(collection)
)
console.log(`\nRarity data written in src/data/collection-rarities.json\n`)
}
if (argv.db) {
console.log('\nInitializing database.')
seed(collection).then(() => {
console.log(`\nDatabase initialized.`)
})
}
}
+86
View File
@@ -0,0 +1,86 @@
export interface Meta {
numTokens: number
traitTypes: TraitsMeta[]
stats: Stats
}
export interface TraitsMeta {
trait_type: string
attributes: Attribute[]
stats: Stats
numValues: number
}
export interface Stats {
max: number
min: number
mean: number
std: number
}
export interface Collection extends Meta {
tokens: TokenWithRarity[]
}
export type Token = {
id: number
name: string
image?: string
image_data?: string
description?: string
attributes: Attribute[]
external_url?: string
animation_url?: string
background_color?: string
}
export interface Attribute {
trait_type: string
value: string
rarity_score: number
rarity_score_normalized?: number
percentile: number
count: number
}
export interface RarityFields {
rarity_score?: number
percentile?: number
count?: number
}
export interface MissingTrait {
trait_type: string
rarity_score: number
percentile: number
}
export interface TokenWithRarity extends Token {
rank: number
rarity_score: number
rarity_score_normalized: number
rank_normalized: number
}
export type GetMissingTraitIdentifier = (
traitType?: string
) => string | undefined
export type Weights = { [traitType: string]: number }
export interface Config {
weights?: Weights
getMissingTraitIdentifier?: GetMissingTraitIdentifier
attributesFieldName?: string
}
export type AttributeWithRarity = Attribute & RarityFields
export type IsMissing = <T extends Token = Token>(
trait_type: string,
token: T
) => boolean
export type GetIsMissing = (
getMissingTraitIdentifier: GetMissingTraitIdentifier
) => IsMissing
+28
View File
@@ -0,0 +1,28 @@
import { GetIsMissing } from './types'
export const getFileName = (
collectionName: string | undefined,
postfix?: string
) => {
return (
(collectionName ?? 'collection').split(' ').join('') +
(postfix ?? '') +
'.json'
)
}
/**
* @param traitType
* @param token
* @param missingTraitIdentifier Value of the trait type that indicates that it is missing. `undefined` (default) if there is no specific identifier and the {trait_type: string, value: string} entry is simply omitted.
* @returns true if the attribute is not found or if the value of the attribute is equal to the missingTraitIdentifier
*/
export const getIsMissing: GetIsMissing =
(missingTraitIdentifierFn) => (traitType, token) => {
const attribute = token.attributes.find(
(att) => att.trait_type === traitType
)
return !attribute || attribute.value === missingTraitIdentifierFn(traitType)
}
export const defaultMissingTraitIdentifier = () => undefined
+4 -4
View File
@@ -9,11 +9,11 @@
"ES6",
"ES2020",
"esnext"
], /* Specify library files to be included in the compilation. */
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true,
// "outFile": "./", /* Concatenate and emit output to single file. */
@@ -44,7 +44,7 @@
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
@@ -67,4 +67,4 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
}
+643
View File
@@ -0,0 +1,643 @@
Arguments:
/Users/mikko/.nvm/versions/node/v14.17.0/bin/node /Users/mikko/.nvm/versions/node/v14.17.0/bin/yarn add @graphql-tools
PATH:
/Users/mikko/google-cloud-sdk/bin:/Users/mikko/.nvm/versions/node/v14.17.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/mikko/.cargo/bin
Yarn version:
1.22.10
Node version:
14.17.0
Platform:
darwin x64
Trace:
Error: https://registry.yarnpkg.com/@graphql-tools: Request "https://registry.yarnpkg.com/@graphql-tools" returned a 405
at Request.params.callback [as _callback] (/Users/mikko/.nvm/versions/node/v14.17.0/lib/node_modules/yarn/lib/cli.js:66997:18)
at Request.self.callback (/Users/mikko/.nvm/versions/node/v14.17.0/lib/node_modules/yarn/lib/cli.js:140662:22)
at Request.emit (events.js:376:20)
at Request.<anonymous> (/Users/mikko/.nvm/versions/node/v14.17.0/lib/node_modules/yarn/lib/cli.js:141634:10)
at Request.emit (events.js:376:20)
at IncomingMessage.<anonymous> (/Users/mikko/.nvm/versions/node/v14.17.0/lib/node_modules/yarn/lib/cli.js:141556:12)
at Object.onceWrapper (events.js:482:28)
at IncomingMessage.emit (events.js:388:22)
at endReadableNT (internal/streams/readable.js:1336:12)
at processTicksAndRejections (internal/process/task_queues.js:82:21)
npm manifest:
{
"name": "rarity-analyser",
"version": "1.0.0",
"main": "index.js",
"author": "mikkoo.eth",
"license": "MIT",
"private": false,
"scripts": {
"seed": "npx prisma db seed"
},
"dependencies": {
"@prisma/client": "^3.0.2",
"@types/express": "^4.17.13",
"@types/node": "^16.7.13",
"express": "^4.17.1",
"express-graphql": "^0.12.0",
"prisma": "3.0.1",
"ts-node": "^10.2.1",
"typescript": "^4.4.2"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
yarn manifest:
No manifest
Lockfile:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
"@cspotcode/source-map-support@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960"
integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@prisma/client@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1"
integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ==
dependencies:
"@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779"
integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ==
"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621"
integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg==
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@types/body-parser@*":
version "1.19.1"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/express-serve-static-core@^4.17.18":
version "4.17.24"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07"
integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.13":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.18"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/node@*":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@^16.7.13":
version "16.7.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.13.tgz#86fae356b03b5a12f2506c6cf6cd9287b205973f"
integrity sha512-pLUPDn+YG3FYEt/pHI74HmnJOWzeR+tOIQzUx93pi9M7D8OE7PSLr97HboXwk5F+JS+TLtWuzCOW97AHjmOXXA==
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/range-parser@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/serve-static@*":
version "1.13.10"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
accepts@^1.3.7, accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1:
version "8.5.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@^1.0.4, content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express-graphql@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.12.0.tgz#58deabc309909ca2c9fe2f83f5fbe94429aa23df"
integrity sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==
dependencies:
accepts "^1.3.7"
content-type "^1.0.4"
http-errors "1.8.0"
raw-body "^2.4.1"
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@1.7.3, http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.49.0:
version "1.49.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
mime-types@~2.1.24:
version "2.1.32"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
dependencies:
mime-db "1.49.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
prisma@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.1.tgz#7f52b6533b562d0bb32e1b74403b146ee6d89029"
integrity sha512-ENmYAopd56nkds5/IOSTGixbkbUN2QdEzB4cp/mtaGB/G0OArbP6cnbA/9u02Pe29RdErbNOoIdCGASjpItJwQ==
dependencies:
"@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
proxy-addr@~2.0.5:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
dependencies:
bytes "3.1.0"
http-errors "1.7.3"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
ts-node@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5"
integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==
dependencies:
"@cspotcode/source-map-support" "0.6.1"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
yn "3.1.1"
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+2379
View File
File diff suppressed because it is too large Load Diff