2022-01-05 05:51:26 +00:00
|
|
|
import * as admin from 'firebase-admin'
|
2022-05-26 21:37:51 +00:00
|
|
|
import { z } from 'zod'
|
2022-05-12 15:07:10 +00:00
|
|
|
|
2022-03-09 17:08:57 +00:00
|
|
|
import {
|
|
|
|
Contract,
|
2022-07-26 23:44:51 +00:00
|
|
|
CPMMBinaryContract,
|
2022-06-01 02:42:35 +00:00
|
|
|
FreeResponseContract,
|
2022-03-09 17:08:57 +00:00
|
|
|
MAX_QUESTION_LENGTH,
|
|
|
|
MAX_TAG_LENGTH,
|
2022-07-28 02:40:33 +00:00
|
|
|
MultipleChoiceContract,
|
2022-06-01 02:42:35 +00:00
|
|
|
NumericContract,
|
2022-05-19 17:42:03 +00:00
|
|
|
OUTCOME_TYPES,
|
2022-08-20 19:31:32 +00:00
|
|
|
VISIBILITIES,
|
2022-05-15 17:39:42 +00:00
|
|
|
} from '../../common/contract'
|
|
|
|
import { slugify } from '../../common/util/slugify'
|
|
|
|
import { randomString } from '../../common/util/random'
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
import { isProd } from './utils'
|
2022-09-27 22:30:07 +00:00
|
|
|
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-08-25 11:51:56 +00:00
|
|
|
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
|
2022-02-17 23:00:19 +00:00
|
|
|
import {
|
2022-08-25 11:51:56 +00:00
|
|
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
2022-03-15 22:27:51 +00:00
|
|
|
getCpmmInitialLiquidity,
|
2022-02-17 23:00:19 +00:00
|
|
|
getFreeAnswerAnte,
|
2022-07-28 02:40:33 +00:00
|
|
|
getMultipleChoiceAntes,
|
2022-05-19 17:42:03 +00:00
|
|
|
getNumericAnte,
|
2022-08-25 11:51:56 +00:00
|
|
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
2022-05-15 17:39:42 +00:00
|
|
|
} from '../../common/antes'
|
2022-07-28 02:40:33 +00:00
|
|
|
import { Answer, getNoneAnswer } from '../../common/answer'
|
2022-05-19 17:42:03 +00:00
|
|
|
import { getNewContract } from '../../common/new-contract'
|
|
|
|
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
2022-06-11 00:51:55 +00:00
|
|
|
import { User } from '../../common/user'
|
2022-08-02 03:15:09 +00:00
|
|
|
import { Group, GroupLink, MAX_ID_LENGTH } from '../../common/group'
|
2022-07-02 19:37:59 +00:00
|
|
|
import { getPseudoProbability } from '../../common/pseudo-numeric'
|
Rich content (#620)
* Add TipTap editor and renderer components
* Change market description editor to rich text
* Type description as JSON, fix string-based logic
- Delete make-predictions.tsx
- Delete feed logic that showed descriptions
* wip Fix API validation
* fix type error
* fix extension import (backend)
In firebase, typescript compiles imports into common js imports
like `const StarterKit = require("@tiptap/starter-kit")`
Even though StarterKit is exported from the cjs file, it gets imported
as undefined. But it magically works if we import *
If you're reading this in the future, consider replacing StarterKit with
the entire list of extensions.
* Stop load on fail create market, improve warning
* Refactor editor as hook / fix infinite submit bug
Move state of editor back up to parent
We have to do this later anyways to allow parent to edit
* Add images - display, paste + uploading
* add uploading state of image
* Fix placeholder, misc styling
min height, quote
* Fix appending to description
* code review fixes: rename, refactor, chop carets
* Add hint & upload button on new lines
- bump to Tailwind 3.1 for arbitrary variants
* clean up, run prettier
* rename FileButton to FileUploadButton
* add image extension as functions dependency
2022-07-13 18:58:22 +00:00
|
|
|
import { JSONContent } from '@tiptap/core'
|
2022-08-02 03:15:09 +00:00
|
|
|
import { uniq, zip } from 'lodash'
|
|
|
|
import { Bet } from '../../common/bet'
|
2022-10-06 03:18:19 +00:00
|
|
|
import { FieldValue, Transaction } from 'firebase-admin/firestore'
|
Rich content (#620)
* Add TipTap editor and renderer components
* Change market description editor to rich text
* Type description as JSON, fix string-based logic
- Delete make-predictions.tsx
- Delete feed logic that showed descriptions
* wip Fix API validation
* fix type error
* fix extension import (backend)
In firebase, typescript compiles imports into common js imports
like `const StarterKit = require("@tiptap/starter-kit")`
Even though StarterKit is exported from the cjs file, it gets imported
as undefined. But it magically works if we import *
If you're reading this in the future, consider replacing StarterKit with
the entire list of extensions.
* Stop load on fail create market, improve warning
* Refactor editor as hook / fix infinite submit bug
Move state of editor back up to parent
We have to do this later anyways to allow parent to edit
* Add images - display, paste + uploading
* add uploading state of image
* Fix placeholder, misc styling
min height, quote
* Fix appending to description
* code review fixes: rename, refactor, chop carets
* Add hint & upload button on new lines
- bump to Tailwind 3.1 for arbitrary variants
* clean up, run prettier
* rename FileButton to FileUploadButton
* add image extension as functions dependency
2022-07-13 18:58:22 +00:00
|
|
|
|
|
|
|
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
|
|
|
z.intersection(
|
|
|
|
z.record(z.any()),
|
|
|
|
z.object({
|
|
|
|
type: z.string().optional(),
|
|
|
|
attrs: z.record(z.any()).optional(),
|
|
|
|
content: z.array(descScehma).optional(),
|
|
|
|
marks: z
|
|
|
|
.array(
|
|
|
|
z.intersection(
|
|
|
|
z.record(z.any()),
|
|
|
|
z.object({
|
|
|
|
type: z.string(),
|
|
|
|
attrs: z.record(z.any()).optional(),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.optional(),
|
|
|
|
text: z.string().optional(),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
)
|
2022-01-05 05:51:26 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const bodySchema = z.object({
|
|
|
|
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
|
2022-08-16 22:01:03 +00:00
|
|
|
description: descScehma.or(z.string()).optional(),
|
2022-05-26 21:37:51 +00:00
|
|
|
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
|
|
|
|
closeTime: zTimestamp().refine(
|
|
|
|
(date) => date.getTime() > new Date().getTime(),
|
|
|
|
'Close time must be in the future.'
|
|
|
|
),
|
|
|
|
outcomeType: z.enum(OUTCOME_TYPES),
|
2022-06-22 16:35:50 +00:00
|
|
|
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
|
2022-08-20 19:31:32 +00:00
|
|
|
visibility: z.enum(VISIBILITIES).optional(),
|
2022-05-26 21:37:51 +00:00
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const binarySchema = z.object({
|
|
|
|
initialProb: z.number().min(1).max(99),
|
|
|
|
})
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-07-11 16:38:51 +00:00
|
|
|
const finite = () =>
|
|
|
|
z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
|
2022-07-02 19:37:59 +00:00
|
|
|
|
2022-05-26 21:37:51 +00:00
|
|
|
const numericSchema = z.object({
|
2022-07-02 19:37:59 +00:00
|
|
|
min: finite(),
|
|
|
|
max: finite(),
|
|
|
|
initialValue: finite(),
|
|
|
|
isLogScale: z.boolean().optional(),
|
2022-05-26 21:37:51 +00:00
|
|
|
})
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-07-28 02:40:33 +00:00
|
|
|
const multipleChoiceSchema = z.object({
|
|
|
|
answers: z.string().trim().min(1).array().min(2),
|
|
|
|
})
|
|
|
|
|
2022-09-27 22:30:07 +00:00
|
|
|
export const createmarket = newEndpoint({}, (req, auth) => {
|
|
|
|
return createMarketHelper(req.body, auth)
|
|
|
|
})
|
|
|
|
|
|
|
|
export async function createMarketHelper(body: any, auth: AuthedUser) {
|
2022-08-20 19:31:32 +00:00
|
|
|
const {
|
|
|
|
question,
|
|
|
|
description,
|
|
|
|
tags,
|
|
|
|
closeTime,
|
|
|
|
outcomeType,
|
|
|
|
groupId,
|
|
|
|
visibility = 'public',
|
2022-09-27 22:30:07 +00:00
|
|
|
} = validate(bodySchema, body)
|
2022-05-19 17:42:03 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
return await firestore.runTransaction(async (trans) => {
|
|
|
|
let min, max, initialProb, isLogScale, answers
|
2022-07-02 19:37:59 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
|
|
|
let initialValue
|
|
|
|
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
|
|
|
|
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
|
|
|
|
throw new APIError(400, 'Invalid range.')
|
2022-07-02 19:37:59 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
initialProb =
|
|
|
|
getPseudoProbability(initialValue, min, max, isLogScale) * 100
|
2022-07-11 16:38:51 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (initialProb < 1 || initialProb > 99)
|
|
|
|
if (outcomeType === 'PSEUDO_NUMERIC')
|
|
|
|
throw new APIError(
|
|
|
|
400,
|
|
|
|
`Initial value is too ${initialProb < 1 ? 'low' : 'high'}`
|
|
|
|
)
|
|
|
|
else throw new APIError(400, 'Invalid initial probability.')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (outcomeType === 'BINARY') {
|
|
|
|
;({ initialProb } = validate(binarySchema, body))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (outcomeType === 'MULTIPLE_CHOICE') {
|
|
|
|
;({ answers } = validate(multipleChoiceSchema, body))
|
|
|
|
}
|
|
|
|
|
|
|
|
const userDoc = await trans.get(firestore.collection('users').doc(auth.uid))
|
|
|
|
if (!userDoc.exists) {
|
|
|
|
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
|
|
|
}
|
|
|
|
const user = userDoc.data() as User
|
|
|
|
|
|
|
|
const ante = FIXED_ANTE
|
|
|
|
const deservesFreeMarket =
|
|
|
|
(user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX
|
|
|
|
// TODO: this is broken because it's not in a transaction
|
|
|
|
if (ante > user.balance && !deservesFreeMarket)
|
|
|
|
throw new APIError(400, `Balance must be at least ${ante}.`)
|
|
|
|
|
|
|
|
let group: Group | null = null
|
|
|
|
if (groupId) {
|
|
|
|
const groupDocRef = firestore.collection('groups').doc(groupId)
|
|
|
|
const groupDoc = await trans.get(groupDocRef)
|
|
|
|
if (!groupDoc.exists) {
|
|
|
|
throw new APIError(400, 'No group exists with the given group ID.')
|
|
|
|
}
|
|
|
|
|
|
|
|
group = groupDoc.data() as Group
|
|
|
|
const groupMembersSnap = await trans.get(
|
|
|
|
firestore.collection(`groups/${groupId}/groupMembers`)
|
|
|
|
)
|
|
|
|
const groupMemberDocs = groupMembersSnap.docs.map(
|
|
|
|
(doc) => doc.data() as { userId: string; createdTime: number }
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
!groupMemberDocs.map((m) => m.userId).includes(user.id) &&
|
|
|
|
!group.anyoneCanJoin &&
|
|
|
|
group.creatorId !== user.id
|
|
|
|
) {
|
2022-07-26 23:44:51 +00:00
|
|
|
throw new APIError(
|
|
|
|
400,
|
2022-10-06 03:18:19 +00:00
|
|
|
'User must be a member/creator of the group or group must be open to add markets to it.'
|
2022-07-26 23:44:51 +00:00
|
|
|
)
|
2022-10-06 03:18:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
const slug = await getSlug(trans, question)
|
|
|
|
const contractRef = firestore.collection('contracts').doc()
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
'creating contract for',
|
|
|
|
user.username,
|
|
|
|
'on',
|
|
|
|
question,
|
|
|
|
'ante:',
|
|
|
|
ante || 0
|
|
|
|
)
|
2022-07-28 02:40:33 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
// convert string descriptions into JSONContent
|
|
|
|
const newDescription =
|
|
|
|
!description || typeof description === 'string'
|
|
|
|
? {
|
|
|
|
type: 'doc',
|
|
|
|
content: [
|
|
|
|
{
|
|
|
|
type: 'paragraph',
|
|
|
|
content: [{ type: 'text', text: description || ' ' }],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
: description
|
|
|
|
|
|
|
|
const contract = getNewContract(
|
|
|
|
contractRef.id,
|
|
|
|
slug,
|
|
|
|
user,
|
|
|
|
question,
|
|
|
|
outcomeType,
|
|
|
|
newDescription,
|
|
|
|
initialProb ?? 0,
|
|
|
|
ante,
|
|
|
|
closeTime.getTime(),
|
|
|
|
tags ?? [],
|
|
|
|
NUMERIC_BUCKET_COUNT,
|
|
|
|
min ?? 0,
|
|
|
|
max ?? 0,
|
|
|
|
isLogScale ?? false,
|
|
|
|
answers ?? [],
|
|
|
|
visibility
|
|
|
|
)
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const providerId = deservesFreeMarket
|
|
|
|
? isProd()
|
|
|
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
|
|
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
|
|
|
: user.id
|
2022-07-28 02:40:33 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (ante) {
|
|
|
|
const delta = FieldValue.increment(-ante)
|
|
|
|
const providerDoc = firestore.collection('users').doc(providerId)
|
|
|
|
await trans.update(providerDoc, { balance: delta, totalDeposits: delta })
|
2022-08-16 05:12:43 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (deservesFreeMarket) {
|
|
|
|
await trans.update(firestore.collection('users').doc(user.id), {
|
|
|
|
freeMarketsCreated: FieldValue.increment(1),
|
|
|
|
})
|
2022-08-16 05:12:43 +00:00
|
|
|
}
|
2022-06-22 22:19:17 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
await contractRef.create(contract)
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (group != null) {
|
|
|
|
const groupContractsSnap = await trans.get(
|
|
|
|
firestore.collection(`groups/${groupId}/groupContracts`)
|
|
|
|
)
|
|
|
|
const groupContracts = groupContractsSnap.docs.map(
|
|
|
|
(doc) => doc.data() as { contractId: string; createdTime: number }
|
|
|
|
)
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
|
|
|
|
await createGroupLinks(trans, group, [contractRef.id], auth.uid)
|
|
|
|
|
|
|
|
const groupContractRef = firestore
|
|
|
|
.collection(`groups/${groupId}/groupContracts`)
|
|
|
|
.doc(contract.id)
|
|
|
|
|
|
|
|
await trans.set(groupContractRef, {
|
|
|
|
contractId: contract.id,
|
|
|
|
createdTime: Date.now(),
|
|
|
|
})
|
|
|
|
}
|
2022-08-02 03:15:09 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
|
|
|
|
const liquidityDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/liquidity`)
|
|
|
|
.doc()
|
2022-05-20 02:06:50 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const lp = getCpmmInitialLiquidity(
|
|
|
|
providerId,
|
|
|
|
contract as CPMMBinaryContract,
|
|
|
|
liquidityDoc.id,
|
|
|
|
ante
|
|
|
|
)
|
2022-05-20 02:06:50 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
await trans.set(liquidityDoc, lp)
|
|
|
|
} else if (outcomeType === 'MULTIPLE_CHOICE') {
|
|
|
|
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
|
|
|
|
const betDocs = (answers ?? []).map(() => betCol.doc())
|
2022-07-28 02:40:33 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
|
|
|
|
const answerDocs = (answers ?? []).map((_, i) =>
|
|
|
|
answerCol.doc(i.toString())
|
|
|
|
)
|
2022-07-28 02:40:33 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const { bets, answerObjects } = getMultipleChoiceAntes(
|
|
|
|
user,
|
|
|
|
contract as MultipleChoiceContract,
|
|
|
|
answers ?? [],
|
|
|
|
betDocs.map((bd) => bd.id)
|
|
|
|
)
|
2022-07-28 02:40:33 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
await Promise.all(
|
|
|
|
zip(bets, betDocs).map(([bet, doc]) =>
|
|
|
|
doc ? trans.create(doc, bet as Bet) : undefined
|
|
|
|
)
|
|
|
|
)
|
|
|
|
await Promise.all(
|
|
|
|
zip(answerObjects, answerDocs).map(([answer, doc]) =>
|
|
|
|
doc ? trans.create(doc, answer as Answer) : undefined
|
|
|
|
)
|
|
|
|
)
|
|
|
|
await trans.update(contractRef, { answers: answerObjects })
|
|
|
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
|
|
const noneAnswerDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/answers`)
|
|
|
|
.doc('0')
|
|
|
|
|
|
|
|
const noneAnswer = getNoneAnswer(contract.id, user)
|
|
|
|
await trans.set(noneAnswerDoc, noneAnswer)
|
|
|
|
|
|
|
|
const anteBetDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const anteBet = getFreeAnswerAnte(
|
|
|
|
providerId,
|
|
|
|
contract as FreeResponseContract,
|
|
|
|
anteBetDoc.id
|
|
|
|
)
|
|
|
|
await trans.set(anteBetDoc, anteBet)
|
|
|
|
} else if (outcomeType === 'NUMERIC') {
|
|
|
|
const anteBetDoc = firestore
|
|
|
|
.collection(`contracts/${contract.id}/bets`)
|
|
|
|
.doc()
|
|
|
|
|
|
|
|
const anteBet = getNumericAnte(
|
|
|
|
providerId,
|
|
|
|
contract as NumericContract,
|
|
|
|
ante,
|
|
|
|
anteBetDoc.id
|
2022-07-28 02:40:33 +00:00
|
|
|
)
|
2022-05-20 02:06:50 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
await trans.set(anteBetDoc, anteBet)
|
|
|
|
}
|
2022-05-17 04:43:40 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
return contract
|
|
|
|
})
|
2022-09-27 22:30:07 +00:00
|
|
|
}
|
2022-01-05 05:51:26 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const getSlug = async (trans: Transaction, question: string) => {
|
2022-01-10 22:49:04 +00:00
|
|
|
const proposedSlug = slugify(question)
|
2022-01-05 05:51:26 +00:00
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
const preexistingContract = await getContractFromSlug(trans, proposedSlug)
|
2022-01-05 05:51:26 +00:00
|
|
|
|
|
|
|
return preexistingContract
|
|
|
|
? proposedSlug + '-' + randomString()
|
|
|
|
: proposedSlug
|
|
|
|
}
|
|
|
|
|
|
|
|
const firestore = admin.firestore()
|
|
|
|
|
2022-10-06 03:18:19 +00:00
|
|
|
async function getContractFromSlug(trans: Transaction, slug: string) {
|
|
|
|
const snap = await trans.get(
|
|
|
|
firestore.collection('contracts').where('slug', '==', slug)
|
|
|
|
)
|
2022-01-05 05:51:26 +00:00
|
|
|
|
|
|
|
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
|
|
|
|
}
|
2022-08-02 03:15:09 +00:00
|
|
|
|
|
|
|
async function createGroupLinks(
|
2022-10-06 03:18:19 +00:00
|
|
|
trans: Transaction,
|
2022-08-02 03:15:09 +00:00
|
|
|
group: Group,
|
|
|
|
contractIds: string[],
|
|
|
|
userId: string
|
|
|
|
) {
|
|
|
|
for (const contractId of contractIds) {
|
2022-10-06 03:18:19 +00:00
|
|
|
const contractRef = firestore.collection('contracts').doc(contractId)
|
|
|
|
const contract = (await trans.get(contractRef)).data() as Contract
|
|
|
|
|
2022-08-02 03:15:09 +00:00
|
|
|
if (!contract?.groupSlugs?.includes(group.slug)) {
|
2022-10-06 03:18:19 +00:00
|
|
|
await trans.update(contractRef, {
|
|
|
|
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
|
|
|
|
})
|
2022-08-02 03:15:09 +00:00
|
|
|
}
|
|
|
|
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
|
2022-10-06 03:18:19 +00:00
|
|
|
await trans.update(contractRef, {
|
|
|
|
groupLinks: [
|
|
|
|
{
|
|
|
|
groupId: group.id,
|
|
|
|
name: group.name,
|
|
|
|
slug: group.slug,
|
|
|
|
userId,
|
|
|
|
createdTime: Date.now(),
|
|
|
|
} as GroupLink,
|
|
|
|
...(contract?.groupLinks ?? []),
|
|
|
|
],
|
|
|
|
})
|
2022-08-02 03:15:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|