From 8a2417de5a55755369cad714cae756065b310547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Thu, 12 Oct 2023 12:16:33 +0200 Subject: [PATCH] refactor api and sdk --- apps/backend/package.json | 1 + .../migration.sql | 5 + apps/backend/prisma/schema.prisma | 3 +- apps/backend/src/app.ts | 6 +- apps/backend/src/middlewares/auth.ts | 11 +- apps/backend/src/middlewares/errors.ts | 16 ++ apps/backend/src/responses/errors.ts | 62 +++--- apps/backend/src/routes/events.ts | 40 ++-- apps/backend/src/routes/profiles.ts | 195 +++++++++--------- apps/backend/src/routes/setup.ts | 9 +- apps/backend/src/services/event.ts | 20 -- apps/backend/src/services/password.ts | 7 - apps/backend/src/services/profile.ts | 96 ++++----- apps/backend/src/types/express.ts | 4 +- bun.lockb | Bin 34112 -> 3632 bytes packages/sdk/index.ts | 97 +++++---- packages/sdk/package.json | 4 +- packages/types/index.ts | 14 +- 18 files changed, 298 insertions(+), 292 deletions(-) create mode 100644 apps/backend/prisma/migrations/20231012082544_external_id_optional/migration.sql create mode 100644 apps/backend/src/middlewares/errors.ts delete mode 100644 apps/backend/src/services/password.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 60421939..de2bcccc 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -17,6 +17,7 @@ "express": "^4.18.2", "morgan": "^1.10.0", "prisma": "^5.4.2", + "random-animal-name": "^0.1.1", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/apps/backend/prisma/migrations/20231012082544_external_id_optional/migration.sql b/apps/backend/prisma/migrations/20231012082544_external_id_optional/migration.sql new file mode 100644 index 00000000..98890914 --- /dev/null +++ b/apps/backend/prisma/migrations/20231012082544_external_id_optional/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "profiles_project_id_external_id_key"; + +-- AlterTable +ALTER TABLE "profiles" ALTER COLUMN "external_id" DROP NOT NULL; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 0365e482..d40ef1e7 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -69,7 +69,7 @@ model Event { model Profile { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - external_id String + external_id String? first_name String? last_name String? email String? @@ -82,7 +82,6 @@ model Profile { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - @@unique([project_id, external_id]) @@map("profiles") } diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 8c6b5304..88c27d64 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -1,9 +1,10 @@ -import express from 'express' +import express, { ErrorRequestHandler } from 'express' import events from './routes/events' import profiles from './routes/profiles' import { authMiddleware } from './middlewares/auth' import morgan from 'morgan' import { setup } from './routes/setup' +import { errorHandler } from './middlewares/errors' const app = express() const port = process.env.PORT || 8080 @@ -21,6 +22,9 @@ if (process.env.SETUP) { app.use(authMiddleware) app.use('/api/sdk', events) app.use('/api/sdk', profiles) + +app.use(errorHandler) + app.listen(port, () => { console.log(`Listening on port ${port}...`) }) diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts index d4d239af..1bf182d2 100644 --- a/apps/backend/src/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -1,14 +1,12 @@ import { NextFunction, Request, Response } from "express" import { db } from "../db" +import { createError } from "../responses/errors" export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const secret = req.headers['mixan-client-secret'] as string | undefined if(!secret) { - return res.status(401).json({ - code: 'UNAUTHORIZED', - message: 'Missing client secret', - }) + return next(createError(401, 'Misisng client secret')) } const client = await db.client.findFirst({ @@ -18,10 +16,7 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc }) if(!client) { - return res.status(401).json({ - code: 'UNAUTHORIZED', - message: 'Invalid client secret', - }) + return next(createError(401, 'Invalid client secret')) } req.client = { diff --git a/apps/backend/src/middlewares/errors.ts b/apps/backend/src/middlewares/errors.ts new file mode 100644 index 00000000..48d89cad --- /dev/null +++ b/apps/backend/src/middlewares/errors.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express"; +import { HttpError } from "../responses/errors"; +import { MixanErrorResponse } from "@mixan/types"; + +export const errorHandler = (error: HttpError | Error, req: Request, res: Response, next: NextFunction) => { + if(error instanceof HttpError) { + return res.status(error.status).json(error.toJson()) + } + + return res.status(500).json({ + code: 500, + status: 'error', + message: error.message || 'Unexpected error occured', + issues: [] + } satisfies MixanErrorResponse); +}; \ No newline at end of file diff --git a/apps/backend/src/responses/errors.ts b/apps/backend/src/responses/errors.ts index 1da9b6f7..1c388b76 100644 --- a/apps/backend/src/responses/errors.ts +++ b/apps/backend/src/responses/errors.ts @@ -1,40 +1,38 @@ import { MixanIssue, - MixanErrorResponse, - MixanIssuesResponse, + MixanErrorResponse } from '@mixan/types' -export function issues(arr: Array): MixanIssuesResponse { - return { - issues: arr.map((item) => { - return { - field: item.field, - message: item.message, - value: item.value, - } - }), +export class HttpError extends Error { + public status: number + public message: string + public issues: MixanIssue[] + + constructor(status: number, message: string | Error, issues?: MixanIssue[]) { + super(message instanceof Error ? message.message : message) + this.status = status + this.message = message instanceof Error ? message.message : message + this.issues = issues || [] + } + + toJson(): MixanErrorResponse { + return { + code: this.status, + status: 'error', + message: this.message, + issues: this.issues, + } } } -export function makeError(error: unknown): MixanErrorResponse { - if (error instanceof Error) { - return { - code: 'Error', - message: error.message, - } - } - - // @ts-ignore - if ('message' in error) { - return { - code: 'UnknownError', - // @ts-ignore - message: error.message, - } - } - - return { - code: 'UnknownError', - message: 'Unknown error', - } +export function createIssues(arr: Array) { + throw new HttpError(400, 'Issues', arr) +} + +export function createError(status = 500, error: unknown | Error | string) { + if(error instanceof Error || typeof error === 'string') { + return new HttpError(status, error) + } + + return new HttpError(500, 'Unexpected error occured') } diff --git a/apps/backend/src/routes/events.ts b/apps/backend/src/routes/events.ts index 3e010740..79f2a044 100644 --- a/apps/backend/src/routes/events.ts +++ b/apps/backend/src/routes/events.ts @@ -1,39 +1,41 @@ -import {Router} from 'express' +import {NextFunction, Response, Router} from 'express' import { db } from '../db'; import { MixanRequest } from '../types/express'; import { EventPayload } from '@mixan/types'; -import { getEvents, getProfileIdFromEvents } from '../services/event'; +import { getEvents } from '../services/event'; import { success } from '../responses/success'; -import { makeError } from '../responses/errors'; const router = Router(); type PostRequest = MixanRequest> -router.get('/events', async (req, res) => { +router.get('/events', async (req, res, next) => { try { const events = await getEvents(req.client.project_id) res.json(success(events)) } catch (error) { - res.json(makeError(error)) + next(error) } }) -router.post('/events', async (req: PostRequest, res) => { - const projectId = req.client.project_id - const profileId = await getProfileIdFromEvents(projectId, req.body) +router.post('/events', async (req: PostRequest, res: Response, next: NextFunction) => { + try { + const projectId = req.client.project_id + + await db.event.createMany({ + data: req.body.map((event) => ({ + name: event.name, + properties: event.properties, + createdAt: event.time, + project_id: projectId, + profile_id: event.profileId, + })) + }) - await db.event.createMany({ - data: req.body.map(event => ({ - name: event.name, - properties: event.properties, - createdAt: event.time, - project_id: projectId, - profile_id: profileId, - })) - }) - - res.status(201).json(success()) + res.status(201).json(success()) + } catch (error) { + next(error) + } }) export default router \ No newline at end of file diff --git a/apps/backend/src/routes/profiles.ts b/apps/backend/src/routes/profiles.ts index 2dafdc36..ecba0352 100644 --- a/apps/backend/src/routes/profiles.ts +++ b/apps/backend/src/routes/profiles.ts @@ -1,132 +1,131 @@ -import { Router } from 'express' +import { NextFunction, Response, Router } from 'express' import { db } from '../db' import { MixanRequest } from '../types/express' -import { - createProfile, - getProfileByExternalId, - updateProfile, -} from '../services/profile' +import { getProfile, tickProfileProperty } from '../services/profile' import { ProfileDecrementPayload, ProfileIncrementPayload, ProfilePayload, } from '@mixan/types' -import { issues } from '../responses/errors' import { success } from '../responses/success' +import randomAnimalName from 'random-animal-name' const router = Router() type PostRequest = MixanRequest -router.get('/profiles', async (req, res) => { - res.json(success(await db.profile.findMany({ - where: { - project_id: req.client.project_id, - }, - }))) -}) - -router.post('/profiles', async (req: PostRequest, res) => { - const body = req.body - const projectId = req.client.project_id - const profile = await getProfileByExternalId(projectId, body.id) - if (profile) { - await updateProfile(projectId, body.id, body, profile) - } else { - await createProfile(projectId, body) +router.get('/profiles', async (req, res, next) => { + try { + res.json( + success( + await db.profile.findMany({ + where: { + project_id: req.client.project_id, + }, + }) + ) + ) + } catch (error) { + next(error) } - - res.status(profile ? 200 : 201).json(success()) }) router.post( - '/profiles/increment', - async (req: MixanRequest, res) => { - const body = req.body - const projectId = req.client.project_id - const profile = await getProfileByExternalId(projectId, body.id) - - if (profile) { - const existingProperties = ( - typeof profile.properties === 'object' ? profile.properties || {} : {} - ) as Record - const value = - body.name in existingProperties ? existingProperties[body.name] : 0 - const properties = { - ...existingProperties, - [body.name]: value + body.value, - } - - if (typeof value !== 'number') { - return res.status(400).json( - issues([ - { - field: 'name', - message: 'Property is not a number', - value, - }, - ]) - ) - } - - await db.profile.updateMany({ - where: { - external_id: String(body.id), - project_id: req.client.project_id, - }, + '/profiles', + async ( + req: MixanRequest<{ + id: string + properties?: Record + }>, + res: Response, + next: NextFunction + ) => { + try { + const projectId = req.client.project_id + const { id, properties } = req.body + const profile = await db.profile.create({ data: { - properties, + id, + external_id: null, + email: null, + first_name: randomAnimalName(), + last_name: null, + avatar: null, + properties: { + ...(properties || {}), + }, + project_id: projectId, }, }) - } - res.status(200).json(success()) + res.status(201).json(success(profile)) + } catch (error) { + next(error) + } } ) -router.post( - '/profiles/decrement', - async (req: MixanRequest, res) => { - const body = req.body - const projectId = req.client.project_id - const profile = await getProfileByExternalId(projectId, body.id) - +router.put('/profiles/:id', async (req: PostRequest, res: Response, next: NextFunction) => { + try { + const profileId = req.params.id + const profile = await getProfile(profileId) + const { body } = req if (profile) { - const existingProperties = ( - typeof profile.properties === 'object' ? profile.properties || {} : {} - ) as Record - const value = - body.name in existingProperties ? existingProperties[body.name] : 0 - - if (typeof value !== 'number') { - return res.status(400).json( - issues([ - { - field: 'name', - message: 'Property is not a number', - value, - }, - ]) - ) - } - - const properties = { - ...existingProperties, - [body.name]: value - body.value, - } - - await db.profile.updateMany({ + await db.profile.update({ where: { - external_id: String(body.id), - project_id: req.client.project_id, + id: profileId, }, data: { - properties, + external_id: body.id, + email: body.email, + first_name: body.first_name, + last_name: body.last_name, + avatar: body.avatar, + properties: { + ...(typeof profile.properties === 'object' + ? profile.properties || {} + : {}), + ...(body.properties || {}), + }, }, }) - } - res.status(200).json(success()) + res.status(200).json(success()) + } + } catch (error) { + next(error) + } +}) + +router.put( + '/profiles/:id/increment', + async (req: MixanRequest, res: Response, next: NextFunction) => { + try { + await tickProfileProperty({ + name: req.body.name, + tick: req.body.value, + profileId: req.params.id, + }) + res.status(200).json(success()) + } catch (error) { + next(error) + } + } +) + +router.put( + '/profiles/:id/decrement', + async (req: MixanRequest, res: Response, next: NextFunction) => { + try { + await tickProfileProperty({ + name: req.body.name, + tick: -Math.abs(req.body.value), + profileId: req.params.id, + }) + res.status(200).json(success()) + } catch (error) { + next(error) + } } ) diff --git a/apps/backend/src/routes/setup.ts b/apps/backend/src/routes/setup.ts index c65c5d71..8e9527bb 100644 --- a/apps/backend/src/routes/setup.ts +++ b/apps/backend/src/routes/setup.ts @@ -1,9 +1,8 @@ -import { Request, Response } from 'express' +import { NextFunction, Request, Response } from 'express' import { db } from '../db' -import { makeError } from '../responses/errors' import { v4 as uuid } from 'uuid' -export async function setup(req: Request, res: Response) { +export async function setup(req: Request, res: Response, next: NextFunction) { try { const organization = await db.organization.create({ data: { @@ -22,7 +21,7 @@ export async function setup(req: Request, res: Response) { data: { name: 'Acme Website Client', project_id: project.id, - secret: uuid(), + secret: '4bfc4a0b-37e0-4916-b634-95c6a32a2e77', }, }) @@ -32,6 +31,6 @@ export async function setup(req: Request, res: Response) { client, }) } catch (error) { - res.json(makeError(error)) + next(error) } } diff --git a/apps/backend/src/services/event.ts b/apps/backend/src/services/event.ts index 3008e44f..537ac410 100644 --- a/apps/backend/src/services/event.ts +++ b/apps/backend/src/services/event.ts @@ -1,4 +1,3 @@ -import { EventPayload } from "@mixan/types"; import { db } from "../db"; export function getEvents(projectId: string) { @@ -8,22 +7,3 @@ export function getEvents(projectId: string) { } }) } - -export async function getProfileIdFromEvents(projectId: string, events: EventPayload[]) { - const event = events.find(item => !!item.externalId) - if(event?.externalId) { - return db.profile.findUnique({ - where: { - project_id_external_id: { - project_id: projectId, - external_id: event.externalId, - } - } - }).then((res) => { - return res?.id || null - }).catch(() => { - return null - }) - } - return null -} diff --git a/apps/backend/src/services/password.ts b/apps/backend/src/services/password.ts deleted file mode 100644 index a084a350..00000000 --- a/apps/backend/src/services/password.ts +++ /dev/null @@ -1,7 +0,0 @@ -export async function hashPassword(password: string) { - return await Bun.password.hash(password); -} - -export async function verifyPassword(password: string,hashedPassword: string) { - return await Bun.password.verify(password, hashedPassword); -} \ No newline at end of file diff --git a/apps/backend/src/services/profile.ts b/apps/backend/src/services/profile.ts index 363d7854..776afe6d 100644 --- a/apps/backend/src/services/profile.ts +++ b/apps/backend/src/services/profile.ts @@ -1,32 +1,15 @@ -import { EventPayload, ProfilePayload } from "@mixan/types"; -import { db } from "../db"; -import { Prisma } from "@prisma/client"; +import { EventPayload, ProfilePayload } from '@mixan/types' +import { db } from '../db' +import { Prisma } from '@prisma/client' +import { HttpError } from '../responses/errors' -export function createProfile(projectId: string, payload: ProfilePayload) { - const { id, email, first_name, last_name, avatar, properties } = payload - return db.profile.create({ - data:{ - external_id: id, - email, - first_name, - last_name, - avatar, - properties: properties || {}, - project_id: projectId, - } - }) -} +type DbProfile = Exclude, null> -type DbProfile = Exclude, null> - -export function getProfileByExternalId(projectId: string, externalId: string) { - return db.profile.findUnique({ +export function getProfile(id: string) { + return db.profile.findUniqueOrThrow({ where: { - project_id_external_id: { - project_id: projectId, - external_id: externalId, - } - } + id, + }, }) } @@ -34,42 +17,47 @@ export function getProfiles(projectId: string) { return db.profile.findMany({ where: { project_id: projectId, - } + }, }) } -export async function updateProfile(projectId: string, profileId: string, payload: Omit, oldProfile: DbProfile) { - const { email, first_name, last_name, avatar, properties } = payload - return db.profile.update({ +export async function tickProfileProperty({ + profileId, + tick, + name, +}: { + profileId: string + tick: number + name: string +}) { + const profile = await getProfile(profileId) + + if (!profile) { + throw new HttpError(404, `Profile not found ${profileId}`) + } + + const properties = ( + typeof profile.properties === 'object' ? profile.properties || {} : {} + ) as Record + const value = name in properties ? properties[name] : 0 + + if (typeof value !== 'number') { + throw new HttpError(400, `Property "${name}" on user is of type ${typeof value}`) + } + + if (typeof tick !== 'number') { + throw new HttpError(400, `Value is not a number ${tick} (${typeof tick})`) + } + + await db.profile.update({ where: { - project_id_external_id: { - project_id: projectId, - external_id: profileId, - } + id: profileId, }, data: { - email, - first_name, - last_name, - avatar, properties: { - ...(typeof oldProfile.properties === 'object' ? oldProfile.properties || {} : {}), - ...(properties || {}), + ...properties, + [name]: value + tick, }, }, }) } - -export async function getInternalProfileId(profileId?: string | null) { - if(!profileId) { - return null - } - - const profile = await db.profile.findFirst({ - where: { - external_id: profileId, - } - }) - - return profile?.id || null -} \ No newline at end of file diff --git a/apps/backend/src/types/express.ts b/apps/backend/src/types/express.ts index f849b01e..7331a4bb 100644 --- a/apps/backend/src/types/express.ts +++ b/apps/backend/src/types/express.ts @@ -1,3 +1,5 @@ -export type MixanRequest = Omit & { +import { Request } from "express" + +export type MixanRequest = Omit & { body: Body } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 898ec7ef2af6944d4b8714ba5fa43326ef7aa155..73621efd1ea35aeed6c84eeaab5fe77b5d9345ab 100755 GIT binary patch delta 870 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�gOIs5$~ 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 diff --git a/packages/sdk/index.ts b/packages/sdk/index.ts index 352f089e..86d9f714 100644 --- a/packages/sdk/index.ts +++ b/packages/sdk/index.ts @@ -1,7 +1,7 @@ +import { v4 as uuid } from 'uuid' import { EventPayload, MixanErrorResponse, - MixanIssuesResponse, MixanResponse, ProfilePayload, } from '@mixan/types' @@ -12,6 +12,9 @@ type MixanOptions = { batchInterval?: number maxBatchSize?: number verbose?: boolean + saveProfileId: (profileId: string) => void, + getProfileId: () => string | null, + removeProfileId: () => void, } class Fetcher { @@ -25,7 +28,11 @@ class Fetcher { this.logger = options.verbose ? console.log : () => {} } - post(path: string, data: Record) { + post( + path: string, + data: Record, + options: FetchRequestInit = {} + ) { const url = `${this.url}${path}` this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2)) return fetch(url, { @@ -35,30 +42,19 @@ class Fetcher { }, method: 'POST', body: JSON.stringify(data), + ...options, }) .then(async (res) => { const response = await res.json< - MixanIssuesResponse | MixanErrorResponse | MixanResponse + MixanErrorResponse | MixanResponse >() - if ('status' in response && response.status === 'ok') { - return response - } - if ('code' in response) { - this.logger(`Mixan error: [${response.code}] ${response.message}`) + if('status' in response && response.status === 'error') { + this.logger(`Mixan request failed: ${url}`, JSON.stringify(response, null, 2)) return null } - - if ('issues' in response) { - this.logger(`Mixan issues:`) - response.issues.forEach((issue) => { - this.logger(` - ${issue.message} (${issue.value})`) - }) - - return null - } - - return null + + return response }) .catch(() => { return null @@ -99,7 +95,7 @@ class Batcher { return } - if (this.queue.length > this.maxBatchSize) { + if (this.queue.length >= this.maxBatchSize) { this.send() return } @@ -116,16 +112,21 @@ class Batcher { export class Mixan { private fetch: Fetcher private eventBatcher: Batcher - private profile: ProfilePayload | null = null - + private profileId?: string + private options: MixanOptions + private logger: (...args: any[]) => void + constructor(options: MixanOptions) { + this.logger = options.verbose ? console.log : () => {} + this.options = options this.fetch = new Fetcher(options) + this.setAnonymousUser() this.eventBatcher = new Batcher(options, (queue) => { this.fetch.post( '/events', queue.map((item) => ({ ...item, - externalId: item.externalId || this.profile?.id, + profileId: item.profileId || this.profileId || null, })) ) }) @@ -140,18 +141,34 @@ export class Mixan { name, properties, time: this.timestamp(), - externalId: this.profile?.id || null, + profileId: this.profileId || null, }) } + private setAnonymousUser() { + const profileId = this.options.getProfileId() + if(profileId) { + this.profileId = profileId + this.logger('Use existing ID', this.profileId); + } else { + this.profileId = uuid() + this.logger('Create new ID', this.profileId); + this.options.saveProfileId(this.profileId) + this.fetch.post('/profiles', { + id: this.profileId, + properties: {}, + }) + } + } + async setUser(profile: ProfilePayload) { - this.profile = profile - await this.fetch.post('/profiles', profile) + await this.fetch.post(`/profiles/${this.profileId}`, profile, { + method: 'PUT' + }) } async setUserProperty(name: string, value: any) { - await this.fetch.post('/profiles', { - ...this.profile, + await this.fetch.post(`/profiles/${this.profileId}`, { properties: { [name]: value, }, @@ -159,33 +176,41 @@ export class Mixan { } async increment(name: string, value: number = 1) { - if (!this.profile) { + if (!this.profileId) { return } - await this.fetch.post('/profiles/increment', { - id: this.profile.id, + await this.fetch.post(`/profiles/${this.profileId}/increment`, { name, value, + }, { + method: 'PUT' }) } async decrement(name: string, value: number = 1) { - if (!this.profile) { + if (!this.profileId) { return } - await this.fetch.post('/profiles/decrement', { - id: this.profile.id, + await this.fetch.post(`/profiles/${this.profileId}/decrement`, { name, value, + }, { + method: 'PUT' }) } - screenView(route: string, properties?: Record) { - this.event('screen_view', { + async screenView(route: string, properties?: Record) { + await this.event('screen_view', { ...(properties || {}), route, }) } + + clear() { + this.eventBatcher.flush() + this.options.removeProfileId() + this.profileId = undefined + } } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 58c586dd..9797592a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -4,9 +4,11 @@ "type": "module", "module": "index.ts", "dependencies": { - "@mixan/types": "workspace:*" + "@mixan/types": "workspace:*", + "uuid": "^9.0.1" }, "devDependencies": { + "@types/uuid": "^9.0.5", "bun-types": "latest", "typescript": "^5.0.0" } diff --git a/packages/types/index.ts b/packages/types/index.ts index 00f756e1..43fb84f2 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -3,7 +3,7 @@ export type MixanJson = Record export type EventPayload = { name: string time: string - externalId: string | null + profileId: string | null properties: MixanJson } @@ -12,8 +12,8 @@ export type ProfilePayload = { last_name?: string email?: string avatar?: string - id: string - properties: MixanJson + id?: string + properties?: MixanJson } export type ProfileIncrementPayload = { @@ -59,13 +59,11 @@ export type MixanIssue = { value: any } -export type MixanIssuesResponse = { - issues: Array, -} - export type MixanErrorResponse = { - code: string + status: 'error' + code: number message: string + issues: Array } export type MixanResponse = {