graphql: Add slug, market and markets queries

Translate /api/v0/markets, /api/v0/market/[id] and /api/v0/slug/[id]
into their graphql equivalent
This commit is contained in:
joy_void_joy 2022-07-20 17:59:03 +02:00
parent 35a811ceee
commit 85746f0461
16 changed files with 454 additions and 7 deletions

View File

@ -6,13 +6,24 @@ generates:
generated/graphql_api.ts: generated/graphql_api.ts:
config: config:
useIndexSignature: true useIndexSignature: true
contextType: web/lib/api/graphql/types#contextType
inputMaybeValue: undefined | T inputMaybeValue: undefined | T
maybeValue: undefined | T maybeValue: undefined | T
strictScalars: true strictScalars: true
mappers:
Comment: web/lib/api/graphql/types#CommentModel
MarketAnswer: web/lib/api/graphql/types#AnswerModel
Market: web/lib/api/graphql/types#MarketModel
Bet: web/lib/api/graphql/types#BetModel
scalars: scalars:
MarketID: 'string' MarketID: 'string'
MarketAnswerID: 'string'
BetID: 'string'
CommentID: 'string'
UserID: 'string'
Timestamp: 'number'
plugins: plugins:
- 'typescript' - 'typescript'

View File

@ -0,0 +1,14 @@
import type { BetResolvers, Resolvers } from 'web/generated/graphql_api'
const betResolvers: BetResolvers = {
market: async (bet) => bet.contract,
user: async (bet, _, { dataSources }) =>
await dataSources.firebaseAPI.getUser(bet.userId),
}
const resolvers: Resolvers = {
Bet: betResolvers,
}
export default resolvers

View File

@ -0,0 +1,30 @@
import type { CommentResolvers, Resolvers } from 'web/generated/graphql_api'
const commentResolvers: CommentResolvers = {
market: async (comment) => comment.contract,
answers: async (comment, _, { dataSources }) => {
const result = await dataSources.firebaseAPI.listAllCommentAnswers(
comment.id,
comment.contract.id
)
return result.map((el) => ({
...el,
contract: comment.contract,
}))
},
user: async (comment) => ({
id: comment.userId,
avatarUrl: comment.userAvatarUrl,
name: comment.userName,
username: comment.userUsername,
}),
}
const resolvers: Resolvers = {
Comment: commentResolvers,
}
export default resolvers

View File

@ -2,8 +2,18 @@ import type { Resolvers } from 'web/generated/graphql_api'
import { merge } from 'lodash' import { merge } from 'lodash'
import scalarResolvers from './scalars'
import marketsResolvers from './market' import marketsResolvers from './market'
import userResolvers from './user'
import betResolvers from './bet'
import commentResolvers from './comment'
const resolvers = merge([marketsResolvers]) as Resolvers const resolvers = merge([
scalarResolvers,
marketsResolvers,
betResolvers,
userResolvers,
commentResolvers,
]) as Resolvers
export default resolvers export default resolvers

View File

@ -0,0 +1,19 @@
import type { Resolvers } from 'web/generated/graphql_api'
import { merge } from 'lodash'
import scalarResolvers from './scalars'
import marketsResolvers from './market'
import userResolvers from './user'
import betResolvers from './bet'
import commentResolvers from './comment'
const resolvers = merge([
scalarResolvers,
marketsResolvers,
betResolvers,
userResolvers,
commentResolvers,
]) as Resolvers
export default resolvers

View File

@ -1,11 +1,122 @@
import type { QueryResolvers, Resolvers } from 'web/generated/graphql_api' import type {
MarketAnswerResolvers,
MarketResolvers,
QueryResolvers,
Resolvers,
ResolversTypes,
} from 'web/generated/graphql_api'
import type { MarketModel } from '../types'
import { UserInputError } from 'apollo-server-micro'
function contractVerify(contract?: MarketModel) {
if (!contract) {
throw new UserInputError('Contract not found')
}
return contract
}
function augmentContract<T>(contract: MarketModel, l: T[]) {
return l.map((el) => ({
...el,
contract,
}))
}
const answerResolvers: MarketAnswerResolvers = {
creator: (answer) => ({
id: answer.userId,
name: answer.name,
username: answer.username,
avatarUrl: answer.avatarUrl,
}),
probability: (answer, _, { dataSources }) =>
dataSources.firebaseAPI.getOutcomeProbability(answer.contract, answer.id),
market: async (answer) => answer.contract,
}
const marketResolvers: MarketResolvers = {
url: async (contract) =>
`https://manifold.markets/${contract.creatorUsername}/${contract.slug}`,
creator: async (contract) => ({
id: contract.creatorId,
username: contract.creatorUsername,
name: contract.creatorName,
avatarUrl: contract.creatorAvatarUrl,
}),
outcome: async (contract: MarketModel, _, { dataSources }) => {
switch (contract.outcomeType) {
case 'BINARY':
return {
__typename: 'MarketOutcomeBinary',
probability: await dataSources.firebaseAPI.getProbability(contract),
} as ResolversTypes['MarketOutcomeBinary']
case 'FREE_RESPONSE':
return {
__typename: 'MarketOutcomeFreeResponse',
answers: augmentContract(contract, contract.answers),
} as ResolversTypes['MarketOutcomeFreeResponse']
case 'PSEUDO_NUMERIC':
case 'NUMERIC':
return {
__typename: 'MarketOutcomeNumeric',
min: contract.min,
max: contract.max,
} as ResolversTypes['MarketOutcomeNumeric']
}
},
pool: async (contract) => contract.pool.YES + contract.pool.NO || undefined,
closeTime: async (contract) =>
contract.resolutionTime && contract.closeTime
? Math.min(contract.resolutionTime, contract.closeTime)
: contract.closeTime,
bets: async (contract, _, { dataSources }) =>
augmentContract(
contract,
await dataSources.firebaseAPI.listAllBets(contract.id)
),
comments: async (contract, _, { dataSources }) =>
augmentContract(
contract,
await dataSources.firebaseAPI.listAllComments(contract.id)
),
}
const queryResolvers: QueryResolvers = { const queryResolvers: QueryResolvers = {
markets: async () => [], markets: async (_, { before, limit }, { dataSources }) => {
if (limit < 1 || limit > 1000) {
throw new UserInputError('limit must be between 1 and 1000')
}
try {
return await dataSources.firebaseAPI.listAllContracts(limit, before)
} catch (e) {
throw new UserInputError(
'Failed to fetch markets (did you pass an invalid ID as the before parameter?)'
)
}
},
market: async (_, { id }, { dataSources }) =>
contractVerify(await dataSources.firebaseAPI.getContractFromID(id)),
slug: async (_, { url }, { dataSources }) =>
contractVerify(await dataSources.firebaseAPI.getContractFromSlug(url)),
} }
const resolver: Resolvers = { const resolver: Resolvers = {
Query: queryResolvers, Query: queryResolvers,
Market: marketResolvers,
MarketAnswer: answerResolvers,
} }
export default resolver export default resolver

View File

@ -0,0 +1,5 @@
import { TimestampResolver } from 'graphql-scalars'
const resolvers = { Timestamp: TimestampResolver }
export default resolvers

View File

@ -0,0 +1,11 @@
import type { Resolvers, UserResolvers } from 'web/generated/graphql_api'
const userResolvers: UserResolvers = {
id: (user) => user.id,
}
const resolvers: Resolvers = {
User: userResolvers,
}
export default resolvers

View File

@ -0,0 +1,51 @@
scalar BetID
type Sale {
# amount user makes from sale
amount: Int
}
type Bet {
id: BetID!
user: User
market: Market!
# bet size; negative if SELL bet
amount: Int!
outcome: String!
# dynamic parimutuel pool weight; negative if SELL bet
shares: Float!
probBefore: Float!
probAfter: Float!
sale: Sale
# true if this BUY bet has been sold
isSold: Boolean
isAnte: Boolean
createdTime: Timestamp
}
type Mutation {
"""
Places a new bet on behalf of the authorized user.
"""
placeBet(
"""
The amount to bet, in M$, before fees.
"""
amount: Int!
"""
The ID of the contract (market) to bet on.
"""
contractId: MarketID!
"""
The outcome to bet on. For binary markets, this is YES or NO. For free response markets, this is the ID of the free response answer. For numeric markets, this is a string representing the target bucket, and an additional value parameter is required which is a number representing the target value. (Bet on numeric markets at your own peril.)
"""
outcome: String!
): Bet
}

View File

@ -0,0 +1,12 @@
scalar CommentID
type Comment {
id: CommentID!
market: Market!
answerOutcome: Int
text: String!
createdTime: Timestamp!
replyTo: Comment
answers: [Comment!]!
user: User!
}

View File

@ -1,9 +1,155 @@
scalar MarketID scalar MarketID
type Market { type Market {
id: MarketID id: MarketID!
creator: User!
bets: [Bet!]!
comments: [Comment!]!
outcome: MarketOutcome!
createdTime: Timestamp!
# Min of creator's chosen date, and resolutionTime
closeTime: Timestamp
question: String!
description: String!
tags: [String!]
url: String!
pool: Float
volume: Float!
volume7Days: Float!
volume24Hours: Float!
isResolved: Boolean!
resolutionTime: Int
resolution: String
p: Float
totalLiquidity: Float
# Method of market making (cpmm, dpm, etc)
# ... TODO: This should be a enum
mechanism: String!
} }
type Query { type MarketOutcomeBinary {
markets(before: MarketID, limit: Int): [Market] probability: Float!
}
input MarketOutcomeBinaryInput {
probability: Float!
}
scalar MarketAnswerID
type MarketAnswer {
id: MarketAnswerID!
text: String!
market: Market
creator: User!
createdTime: Timestamp!
number: Int!
probability: Float!
}
type MarketOutcomeFreeResponse {
answers: [MarketAnswer!]!
}
type MarketOutcomeNumeric {
min: Float!
max: Float!
}
input MarketOutcomeNumericInput {
min: Float!
max: Float!
}
enum MarketType {
BINARY
FREE_RESPONSE
NUMERIC
}
union MarketOutcome =
MarketOutcomeFreeResponse
| MarketOutcomeNumeric
| MarketOutcomeBinary
type Query {
"""
Lists all markets, ordered by creation date descending.
"""
markets(
"""
The ID of the market before which the list will start.
For example, if you ask for the most recent 10 markets, and then perform a second query for 10 more markets with before=[the id of the 10th market], you will get markets 11 through 20.
"""
before: MarketID
"""
How many markets to return. The maximum and the default is 1000.
"""
limit: Int = 1000
): [Market!]!
"""
Gets information about a single market by ID.
Example request: https://manifold.markets/api/v0/market/3zspH9sSzMlbFQLn9GKR
"""
market(id: MarketID!): Market!
"""
Gets information about a single market by slug (the portion of the URL path after the username).
"""
slug(url: String!): Market!
}
input MarketInput {
"""
The headline question for the market.
"""
question: String!
"""
A long description describing the rules for the market.
"""
description: String!
"""
The time at which the market will close, represented as milliseconds since the epoch.
"""
closeTime: Timestamp
"""
An array of string tags for the market.
"""
tags: [String!]
}
type Mutation {
"""
Creates a new binary market on behalf of the authorized user.
"""
createMarketBinary(
"""
Condition of market at creation
"""
initial: MarketOutcomeBinaryInput
input: MarketInput
): Market
"""
Creates a new free response market on behalf of the authorized user.
"""
createMarketFreeResponse(input: MarketInput): Market
"""
Creates a new numeric market on behalf of the authorized user.
"""
createMarketNumeric(
"""
Condition of market at creation
"""
initial: MarketOutcomeNumericInput
input: MarketInput
): Market
} }

View File

@ -0,0 +1 @@
scalar Timestamp

View File

@ -0,0 +1,8 @@
scalar UserID
type User {
id: UserID!
username: String!
name: String!
avatarUrl: String
}

View File

@ -1,4 +1,14 @@
import type { FirebaseAPI } from './datasources/firebaseAPI' import type { FirebaseAPI } from './datasources/firebaseAPI'
import type { Answer } from 'common/answer'
import type { Bet } from 'common/bet'
import type { Comment } from 'common/comment'
import type { Contract } from 'common/contract'
/* Model types, used internally by resolvers */
export type MarketModel = Contract
export type AnswerModel = Answer & { contract: MarketModel }
export type CommentModel = Comment & { contract: MarketModel }
export type BetModel = Bet & { contract: MarketModel }
/* Context type */ /* Context type */
export type contextType = { export type contextType = {

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"", "dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"",
"devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS,GRAPHQL -c magenta,cyan,yellow \"cross-env FIREBASE_ENV=DEV next dev -p 3000\" \"cross-env FIREBASE_ENV=DEV yarn ts --watch\" \"yarn generate --watch lib/api/graphql/**/**.graphql \"", "devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS,GRAPHQL -c magenta,cyan,yellow \"cross-env FIREBASE_ENV=DEV next dev -p 3000\" \"cross-env FIREBASE_ENV=DEV yarn ts --watch\" \"yarn generate --watch lib/api/graphql/**/**.graphql --watch lib/api/graphql/types.ts \"",
"dev:dev": "yarn devdev", "dev:dev": "yarn devdev",
"dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"", "dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"",
"dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev", "dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev",
@ -44,6 +44,7 @@
"dayjs": "1.10.7", "dayjs": "1.10.7",
"firebase": "9.6.0", "firebase": "9.6.0",
"graphql": "16.5.0", "graphql": "16.5.0",
"graphql-scalars": "1.17.0",
"gridjs": "5.0.2", "gridjs": "5.0.2",
"gridjs-react": "5.0.2", "gridjs-react": "5.0.2",
"lodash": "4.17.21", "lodash": "4.17.21",

View File

@ -8111,6 +8111,13 @@ graphql-request@^4.0.0:
extract-files "^9.0.0" extract-files "^9.0.0"
form-data "^3.0.0" form-data "^3.0.0"
graphql-scalars@1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.17.0.tgz#10e4f0fe44472d475dd72f14412c065fd1b7aff8"
integrity sha512-y1WtSu6jiW5QdDjK3RWMRTdK+xAAtSIq3IxmtnhxzH7bCkHV/z8VZa8fsSG4BcWbjQQtCQYQvMnvbQ+TBCyJRQ==
dependencies:
tslib "~2.3.0"
graphql-tag@^2.11.0: graphql-tag@^2.11.0:
version "2.12.6" version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"