wip: docker
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Ready for docker-compose
|
||||||
|
REDIS_URL="redis://127.0.0.1:6379"
|
||||||
|
DATABASE_URL="postgres://username:password@127.0.0.1:5435/postgres?sslmode=disable"
|
||||||
|
NEXTAUTH_SECRET="secret_sauce"
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
54
README.md
54
README.md
@@ -69,39 +69,35 @@ For pushing events
|
|||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Mixan } from '@mixan/sdk';
|
import { MixanWeb } from '@mixan/sdk-web';
|
||||||
|
|
||||||
const mixan = new Mixan({
|
// import { MixanNative } from '@mixan/sdk-native';
|
||||||
|
|
||||||
|
const mixan = new MixanWeb({
|
||||||
clientId: 'uuid',
|
clientId: 'uuid',
|
||||||
clientSecret: 'uuid',
|
|
||||||
url: 'http://localhost:8080/api/sdk',
|
url: 'http://localhost:8080/api/sdk',
|
||||||
batchInterval: 10000,
|
batchInterval: 10000,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
saveProfileId(id) {
|
trackIp: true,
|
||||||
// Web
|
|
||||||
localStorage.setItem('@profileId', id);
|
|
||||||
// // react-native-mmkv
|
|
||||||
// mmkv.setItem('@profileId', id)
|
|
||||||
},
|
|
||||||
removeProfileId() {
|
|
||||||
// Web
|
|
||||||
localStorage.removeItem('@profileId');
|
|
||||||
// // react-native-mmkv
|
|
||||||
// mmkv.delete('@profileId')
|
|
||||||
},
|
|
||||||
getProfileId() {
|
|
||||||
// Web
|
|
||||||
return localStorage.getItem('@profileId');
|
|
||||||
// // react-native-mmkv
|
|
||||||
// return mmkv.getString('@profileId')
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const mixan = new MixanNative({
|
||||||
|
// clientId: 'uuid',
|
||||||
|
// clientSecret: 'uuid',
|
||||||
|
// url: 'http://localhost:8080/api/sdk',
|
||||||
|
// batchInterval: 10000,
|
||||||
|
// verbose: false,
|
||||||
|
// trackIp: true,
|
||||||
|
// });
|
||||||
|
|
||||||
// Call this before you send any events
|
// Call this before you send any events
|
||||||
// It will create a anonymous profile
|
// It will create a anonymous profile
|
||||||
// This profile will be merged if you call `setUser` in a later stage
|
// This profile will be merged if you call `setUser` in a later stage
|
||||||
mixan.init();
|
mixan.init();
|
||||||
|
|
||||||
|
// tracks all outgoing links as a `link_out` event
|
||||||
|
mixan.trackOutgoingLinks();
|
||||||
|
|
||||||
mixan.setUser({
|
mixan.setUser({
|
||||||
id: 'id',
|
id: 'id',
|
||||||
first_name: 'John',
|
first_name: 'John',
|
||||||
@@ -125,10 +121,13 @@ mixan.event('sign_in', {
|
|||||||
provider: 'gmail',
|
provider: 'gmail',
|
||||||
});
|
});
|
||||||
|
|
||||||
// short hand for 'screen_view', can also take any properties
|
// Screen view for web
|
||||||
mixan.screenView('Profile', {
|
mixan.screenView();
|
||||||
id: '123',
|
|
||||||
// any other properties, url, public
|
// Screen view for native
|
||||||
|
mixan.screenView('Article', {
|
||||||
|
id: '3',
|
||||||
|
title: 'Nice article here',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call this when a user is logged out.
|
// Call this when a user is logged out.
|
||||||
@@ -150,6 +149,11 @@ We use https://cron-job.org (free) to handle our cronjobs, you can use any provi
|
|||||||
- **https://domain.com/api/cron/cache/update** Will update the memory cache
|
- **https://domain.com/api/cron/cache/update** Will update the memory cache
|
||||||
- **https://domain.com/api/cron/events/enrich** Enrich events (adds duration etc)
|
- **https://domain.com/api/cron/events/enrich** Enrich events (adds duration etc)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
1. Run `docker-compose up -d` to get redis and postgres running
|
||||||
|
2. Then `pnpm dev` to boot the web and worker (queue)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev -p 3002",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { MixanWeb } from '@mixan-test/sdk-web';
|
|||||||
|
|
||||||
export const mixan = new MixanWeb({
|
export const mixan = new MixanWeb({
|
||||||
verbose: true,
|
verbose: true,
|
||||||
url: process.env.NEXT_PUBLIC_MIXAN_URL!,
|
url: 'http://localhost:3000/api/sdk',
|
||||||
clientId: process.env.NEXT_PUBLIC_MIXAN_CLIENT_ID!,
|
clientId: '568b4ed1-5d00-4f27-88a7-b8959e6674bd',
|
||||||
clientSecret: process.env.NEXT_PUBLIC_MIXAN_CLIENT_SECRET!,
|
clientSecret: '1e362905-d352-44c4-9263-e037a2ad52fb',
|
||||||
trackIp: true,
|
trackIp: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mixan.init({
|
||||||
|
appVersion: '1.0.0',
|
||||||
|
});
|
||||||
mixan.trackOutgoingLinks();
|
mixan.trackOutgoingLinks();
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { mixan } from '@/analytics';
|
|||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
mixan.init();
|
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
mixan.screenView();
|
||||||
return router.events.on('routeChangeComplete', () => {
|
return router.events.on('routeChangeComplete', () => {
|
||||||
mixan.screenView();
|
mixan.screenView();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
|
||||||
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
|
||||||
# when you add new variables to `.env`.
|
|
||||||
|
|
||||||
# This file will be committed to version control, so make sure not to have any
|
|
||||||
# secrets in it. If you are cloning this repo, create a copy of this file named
|
|
||||||
# ".env" and populate it with your secrets.
|
|
||||||
|
|
||||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
|
||||||
# should be updated accordingly.
|
|
||||||
|
|
||||||
# Prisma
|
|
||||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
|
||||||
DATABASE_URL="file:./db.sqlite"
|
|
||||||
|
|
||||||
# Next Auth
|
|
||||||
# You can generate a new secret on the command line with:
|
|
||||||
# openssl rand -base64 32
|
|
||||||
# https://next-auth.js.org/configuration/options#secret
|
|
||||||
# NEXTAUTH_SECRET=""
|
|
||||||
NEXTAUTH_URL="http://localhost:3000"
|
|
||||||
|
|
||||||
# Next Auth Discord Provider
|
|
||||||
DISCORD_CLIENT_ID=""
|
|
||||||
DISCORD_CLIENT_SECRET=""
|
|
||||||
@@ -7,9 +7,13 @@ await import('./src/env.mjs');
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: [],
|
transpilePackages: ['@mixan/queue'],
|
||||||
eslint: { ignoreDuringBuilds: true },
|
eslint: { ignoreDuringBuilds: true },
|
||||||
typescript: { ignoreBuildErrors: true },
|
typescript: { ignoreBuildErrors: true },
|
||||||
|
experimental: {
|
||||||
|
// Avoid "Critical dependency: the request of a dependency is an expression"
|
||||||
|
serverComponentsExternalPackages: ['bullmq'],
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* If you are using `appDir` then you must comment the below `i18n` config out.
|
* If you are using `appDir` then you must comment the below `i18n` config out.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "prisma generate",
|
"dev": "pnpm with-env next dev",
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
|
"format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "dotenv -e ../../.env -c --"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@mixan/db": "workspace:^",
|
||||||
|
"@mixan/queue": "workspace:^",
|
||||||
"@mixan/types": "workspace:*",
|
"@mixan/types": "workspace:*",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
|
||||||
"@prisma/client": "^5.1.1",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
@@ -83,7 +83,6 @@
|
|||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.1",
|
"prettier-plugin-tailwindcss": "^0.5.1",
|
||||||
"prisma": "^5.1.1",
|
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import type { Project as IProject } from '@prisma/client';
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import type { Project as IProject } from '@mixan/db';
|
||||||
|
|
||||||
import { ProjectActions } from './ProjectActions';
|
import { ProjectActions } from './ProjectActions';
|
||||||
|
|
||||||
export type Project = IProject;
|
export type Project = IProject;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
||||||
import { toast } from '@/components/ui/use-toast';
|
import { toast } from '@/components/ui/use-toast';
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
||||||
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
import { useRefetchActive } from '@/hooks/useRefetchActive';
|
||||||
@@ -144,8 +143,12 @@ export default function CreateProject() {
|
|||||||
name="withCors"
|
name="withCors"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<label className="flex items-center gap-2 text-sm font-medium leading-none mb-4">
|
<label
|
||||||
|
htmlFor="cors"
|
||||||
|
className="flex items-center gap-2 text-sm font-medium leading-none mb-4"
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
id="cors"
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
defaultChecked={field.value}
|
defaultChecked={field.value}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { validateSdkRequest } from '@/server/auth';
|
import { validateSdkRequest } from '@/server/auth';
|
||||||
import { db } from '@/server/db';
|
|
||||||
import { createError, handleError } from '@/server/exceptions';
|
import { createError, handleError } from '@/server/exceptions';
|
||||||
import { tickProfileProperty } from '@/server/services/profile.service';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { mergeDeepRight } from 'ramda';
|
|
||||||
|
|
||||||
|
import { eventsQueue } from '@mixan/queue';
|
||||||
import type { BatchPayload } from '@mixan/types';
|
import type { BatchPayload } from '@mixan/types';
|
||||||
|
|
||||||
interface Request extends NextApiRequest {
|
interface Request extends NextApiRequest {
|
||||||
@@ -28,207 +25,13 @@ export default async function handler(req: Request, res: NextApiResponse) {
|
|||||||
return handleError(res, createError(405, 'Method not allowed'));
|
return handleError(res, createError(405, 'Method not allowed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check client id & secret
|
// Check client id & secret
|
||||||
const projectId = await validateSdkRequest(req, res);
|
const projectId = await validateSdkRequest(req, res);
|
||||||
|
|
||||||
const profileIds = new Set<string>(
|
await eventsQueue.add('batch', {
|
||||||
req.body
|
projectId,
|
||||||
.map((item) => item.payload.profileId)
|
payload: req.body,
|
||||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (profileIds.size === 0) {
|
|
||||||
return res.status(400).json({ status: 'error' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const profiles = await db.profile.findMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: Array.from(profileIds),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
async function getProfile(profileId: string) {
|
|
||||||
const profile = profiles.find((profile) => profile.id === profileId);
|
|
||||||
if (profile) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await db.profile.create({
|
|
||||||
data: {
|
|
||||||
id: profileId,
|
|
||||||
properties: {},
|
|
||||||
project_id: projectId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
profiles.push(created);
|
|
||||||
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedBody: BatchPayload[] = req.body.reduce((acc, item) => {
|
|
||||||
const canMerge =
|
|
||||||
item.type === 'update_profile' || item.type === 'update_session';
|
|
||||||
|
|
||||||
if (!canMerge) {
|
|
||||||
return [...acc, item];
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = acc.findIndex(
|
|
||||||
(i) =>
|
|
||||||
i.type === item.type && i.payload.profileId === item.payload.profileId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (acc[match]) {
|
|
||||||
acc[match]!.payload = mergeDeepRight(acc[match]!.payload, item.payload);
|
|
||||||
} else {
|
|
||||||
acc.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, [] as BatchPayload[]);
|
|
||||||
|
|
||||||
const failedEvents: BatchPayload[] = [];
|
|
||||||
|
|
||||||
for (const item of mergedBody) {
|
|
||||||
try {
|
|
||||||
const { type, payload } = item;
|
|
||||||
const profile = await getProfile(payload.profileId);
|
|
||||||
switch (type) {
|
|
||||||
case 'create_profile': {
|
|
||||||
profile.properties = {
|
|
||||||
...(typeof profile.properties === 'object'
|
|
||||||
? profile.properties ?? {}
|
|
||||||
: {}),
|
|
||||||
...(payload.properties ?? {}),
|
|
||||||
};
|
|
||||||
await db.profile.update({
|
|
||||||
where: {
|
|
||||||
id: payload.profileId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
properties: profile.properties,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'update_profile': {
|
|
||||||
profile.properties = {
|
|
||||||
...(typeof profile.properties === 'object'
|
|
||||||
? profile.properties ?? {}
|
|
||||||
: {}),
|
|
||||||
...(payload.properties ?? {}),
|
|
||||||
};
|
|
||||||
await db.profile.update({
|
|
||||||
where: {
|
|
||||||
id: payload.profileId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
external_id: payload.id,
|
|
||||||
email: payload.email,
|
|
||||||
first_name: payload.first_name,
|
|
||||||
last_name: payload.last_name,
|
|
||||||
avatar: payload.avatar,
|
|
||||||
properties: profile.properties,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'set_profile_property': {
|
|
||||||
if (
|
|
||||||
typeof (profile.properties as Record<string, unknown>)[
|
|
||||||
payload.name
|
|
||||||
] === 'undefined'
|
|
||||||
) {
|
|
||||||
(profile.properties as Record<string, unknown>)[payload.name] =
|
|
||||||
payload.value;
|
|
||||||
|
|
||||||
await db.profile.update({
|
|
||||||
where: {
|
|
||||||
id: payload.profileId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
// @ts-expect-error
|
|
||||||
properties: profile.properties,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'increment': {
|
|
||||||
await tickProfileProperty({
|
|
||||||
profileId: payload.profileId,
|
|
||||||
name: payload.name,
|
|
||||||
tick: payload.value,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'decrement': {
|
|
||||||
await tickProfileProperty({
|
|
||||||
profileId: payload.profileId,
|
|
||||||
name: payload.name,
|
|
||||||
tick: -Math.abs(payload.value),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'event': {
|
|
||||||
await db.event.create({
|
|
||||||
data: {
|
|
||||||
name: payload.name,
|
|
||||||
properties: payload.properties,
|
|
||||||
createdAt: payload.time,
|
|
||||||
project_id: projectId,
|
|
||||||
profile_id: payload.profileId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'update_session': {
|
|
||||||
const session = await db.event.findFirst({
|
|
||||||
where: {
|
|
||||||
profile_id: payload.profileId,
|
|
||||||
project_id: projectId,
|
|
||||||
name: 'session_start',
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (session) {
|
|
||||||
await db.$executeRawUnsafe(
|
|
||||||
`UPDATE events SET properties = '${JSON.stringify(
|
|
||||||
payload.properties
|
|
||||||
)}' || properties WHERE "createdAt" >= '${session.createdAt.toISOString()}' AND profile_id = '${
|
|
||||||
payload.profileId
|
|
||||||
}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to create "${item.type}"`);
|
|
||||||
console.log(' > Payload:', item.payload);
|
|
||||||
console.log(' > Error:', error);
|
|
||||||
failedEvents.push(item);
|
|
||||||
}
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
await db.eventFailed.createMany({
|
|
||||||
data: failedEvents.map((item) => ({
|
|
||||||
data: item as Record<string, any>,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Batch took', Date.now() - time, 'ms', {
|
|
||||||
events: req.body.length,
|
|
||||||
combined: mergedBody.length,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({ status: 'ok' });
|
res.status(200).json({ status: 'ok' });
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ import { createError, handleError } from '@/server/exceptions';
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import randomAnimalName from 'random-animal-name';
|
import randomAnimalName from 'random-animal-name';
|
||||||
|
|
||||||
import type {
|
import type { CreateProfileResponse, ProfilePayload } from '@mixan/types';
|
||||||
CreateProfilePayload,
|
|
||||||
CreateProfileResponse,
|
|
||||||
ProfilePayload,
|
|
||||||
} from '@mixan/types';
|
|
||||||
|
|
||||||
interface Request extends NextApiRequest {
|
interface Request extends NextApiRequest {
|
||||||
body: ProfilePayload | CreateProfilePayload;
|
body: ProfilePayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: Request, res: NextApiResponse) {
|
export default async function handler(req: Request, res: NextApiResponse) {
|
||||||
|
|||||||
32
apps/web/src/pages/api/sdk/test.ts
Normal file
32
apps/web/src/pages/api/sdk/test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
import { eventsQueue } from '@mixan/queue';
|
||||||
|
import type { BatchPayload } from '@mixan/types';
|
||||||
|
|
||||||
|
interface Request extends NextApiRequest {
|
||||||
|
body: BatchPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
responseLimit: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function handler(req: Request, res: NextApiResponse) {
|
||||||
|
eventsQueue.add('batch', {
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
payload: {
|
||||||
|
profileId: 'f8235c6a-c720-4f38-8f6c-b6b7d31e16db',
|
||||||
|
name: 'test',
|
||||||
|
properties: {},
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectId: 'b725eadb-a1fe-4be8-bf0b-9d9bfa6aac12',
|
||||||
|
});
|
||||||
|
res.status(200).json({ status: 'ok' });
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ export const clientRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
cors: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
@@ -50,6 +51,7 @@ export const clientRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
cors: input.cors,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { db } from '@/server/db';
|
|||||||
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
||||||
import { getProjectBySlug } from '@/server/services/project.service';
|
import { getProjectBySlug } from '@/server/services/project.service';
|
||||||
import { slug } from '@/utils/slug';
|
import { slug } from '@/utils/slug';
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { PrismaError } from 'prisma-error-enum';
|
import { PrismaError } from 'prisma-error-enum';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Prisma } from '@mixan/db';
|
||||||
|
|
||||||
export const dashboardRouter = createTRPCRouter({
|
export const dashboardRouter = createTRPCRouter({
|
||||||
get: protectedProcedure
|
get: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import type {
|
|||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { alphabetIds, timeRanges } from '@/utils/constants';
|
import { alphabetIds, timeRanges } from '@/utils/constants';
|
||||||
import { zChartInput } from '@/utils/validation';
|
import { zChartInput } from '@/utils/validation';
|
||||||
import type { Report as DbReport } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { Report as DbReport } from '@mixan/db';
|
||||||
|
|
||||||
function transformFilter(
|
function transformFilter(
|
||||||
filter: Partial<IChartEventFilter>,
|
filter: Partial<IChartEventFilter>,
|
||||||
index: number
|
index: number
|
||||||
@@ -48,7 +49,7 @@ function transformReport(report: DbReport): IChartInput & { id: string } {
|
|||||||
chartType: report.chart_type,
|
chartType: report.chart_type,
|
||||||
interval: report.interval,
|
interval: report.interval,
|
||||||
name: report.name || 'Untitled',
|
name: report.name || 'Untitled',
|
||||||
range: report.range as IChartRange ?? timeRanges['1m'],
|
range: (report.range as IChartRange) ?? timeRanges['1m'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,16 @@ export async function validateSdkRequest(
|
|||||||
throw createError(401, 'Invalid client secret');
|
throw createError(401, 'Invalid client secret');
|
||||||
}
|
}
|
||||||
} else if (client.cors !== '*') {
|
} else if (client.cors !== '*') {
|
||||||
res.setHeader('Access-Control-Allow-Origin', client.cors);
|
const ok = client.cors.split(',').find((origin) => {
|
||||||
|
if (origin === req.headers.origin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (ok) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', String(req.headers.origin));
|
||||||
|
} else {
|
||||||
|
throw createError(401, 'Invalid cors settings');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.project_id;
|
return client.project_id;
|
||||||
|
|||||||
@@ -1,14 +1 @@
|
|||||||
import { env } from '@/env.mjs';
|
export { db } from '@mixan/db';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
|
||||||
prisma: PrismaClient | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const db =
|
|
||||||
globalForPrisma.prisma ??
|
|
||||||
new PrismaClient({
|
|
||||||
log: ['error'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import type {
|
|||||||
zChartType,
|
zChartType,
|
||||||
zTimeInterval,
|
zTimeInterval,
|
||||||
} from '@/utils/validation';
|
} from '@/utils/validation';
|
||||||
import type { Client, Project } from '@prisma/client';
|
|
||||||
import type { TooltipProps } from 'recharts';
|
import type { TooltipProps } from 'recharts';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import type { Client, Project } from '@mixan/db';
|
||||||
|
|
||||||
export type HtmlProps<T> = Omit<
|
export type HtmlProps<T> = Omit<
|
||||||
React.DetailedHTMLProps<React.HTMLAttributes<T>, T>,
|
React.DetailedHTMLProps<React.HTMLAttributes<T>, T>,
|
||||||
'ref'
|
'ref'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Profile } from '@prisma/client';
|
import type { Profile } from '@mixan/db';
|
||||||
|
|
||||||
export function getProfileName(profile: Profile | undefined | null) {
|
export function getProfileName(profile: Profile | undefined | null) {
|
||||||
if (!profile) return '';
|
if (!profile) return '';
|
||||||
|
|||||||
40
apps/worker/package.json
Normal file
40
apps/worker/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@mixan/worker",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"build": "rm -rf dist && tsup",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bull-board/api": "^5.13.0",
|
||||||
|
"@bull-board/express": "^5.13.0",
|
||||||
|
"@mixan/db": "workspace:*",
|
||||||
|
"@mixan/queue": "workspace:*",
|
||||||
|
"bullmq": "^5.1.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"ramda": "^0.29.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mixan/eslint-config": "workspace:*",
|
||||||
|
"@mixan/prettier-config": "workspace:*",
|
||||||
|
"@mixan/tsconfig": "workspace:*",
|
||||||
|
"@mixan/types": "workspace:*",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/ramda": "^0.29.6",
|
||||||
|
"eslint": "^8.48.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"tsup": "^7.2.0",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"@mixan/eslint-config/base"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@mixan/prettier-config"
|
||||||
|
}
|
||||||
29
apps/worker/src/index.ts
Normal file
29
apps/worker/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createBullBoard } from '@bull-board/api';
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
|
import { ExpressAdapter } from '@bull-board/express';
|
||||||
|
import { Worker } from 'bullmq';
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { connection, eventsQueue } from '@mixan/queue';
|
||||||
|
|
||||||
|
import { eventsJob } from './jobs/events';
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
const serverAdapter = new ExpressAdapter();
|
||||||
|
serverAdapter.setBasePath('/');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
new Worker(eventsQueue.name, eventsJob, {
|
||||||
|
connection,
|
||||||
|
});
|
||||||
|
|
||||||
|
createBullBoard({
|
||||||
|
queues: [new BullMQAdapter(eventsQueue)],
|
||||||
|
serverAdapter: serverAdapter,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/', serverAdapter.getRouter());
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`For the UI, open http://localhost:${PORT}/`);
|
||||||
|
});
|
||||||
250
apps/worker/src/jobs/events.ts
Normal file
250
apps/worker/src/jobs/events.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import type { Job } from 'bullmq';
|
||||||
|
import { mergeDeepRight } from 'ramda';
|
||||||
|
|
||||||
|
import { db } from '@mixan/db';
|
||||||
|
import type { EventsQueuePayload } from '@mixan/queue/src/queues';
|
||||||
|
import type { BatchPayload } from '@mixan/types';
|
||||||
|
|
||||||
|
export async function eventsJob(job: Job<EventsQueuePayload>) {
|
||||||
|
const projectId = job.data.projectId;
|
||||||
|
const body = job.data.payload;
|
||||||
|
|
||||||
|
const profileIds = new Set<string>(
|
||||||
|
body
|
||||||
|
.map((item) => item.payload.profileId)
|
||||||
|
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profileIds.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profiles = await db.profile.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: Array.from(profileIds),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getProfile(profileId: string) {
|
||||||
|
const profile = profiles.find((profile) => profile.id === profileId);
|
||||||
|
if (profile) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await db.profile.create({
|
||||||
|
data: {
|
||||||
|
id: profileId,
|
||||||
|
properties: {},
|
||||||
|
project_id: projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
profiles.push(created);
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedBody: BatchPayload[] = body.reduce((acc, item) => {
|
||||||
|
const canMerge =
|
||||||
|
item.type === 'update_profile' || item.type === 'update_session';
|
||||||
|
|
||||||
|
if (!canMerge) {
|
||||||
|
return [...acc, item];
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = acc.findIndex(
|
||||||
|
(i) =>
|
||||||
|
i.type === item.type && i.payload.profileId === item.payload.profileId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (acc[match]) {
|
||||||
|
acc[match]!.payload = mergeDeepRight(acc[match]!.payload, item.payload);
|
||||||
|
} else {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as BatchPayload[]);
|
||||||
|
|
||||||
|
const failedEvents: BatchPayload[] = [];
|
||||||
|
|
||||||
|
for (const item of mergedBody) {
|
||||||
|
try {
|
||||||
|
const { type, payload } = item;
|
||||||
|
const profile = await getProfile(payload.profileId);
|
||||||
|
switch (type) {
|
||||||
|
case 'create_profile': {
|
||||||
|
profile.properties = {
|
||||||
|
...(typeof profile.properties === 'object'
|
||||||
|
? profile.properties ?? {}
|
||||||
|
: {}),
|
||||||
|
...(payload.properties ?? {}),
|
||||||
|
};
|
||||||
|
await db.profile.update({
|
||||||
|
where: {
|
||||||
|
id: payload.profileId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
properties: profile.properties,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update_profile': {
|
||||||
|
profile.properties = {
|
||||||
|
...(typeof profile.properties === 'object'
|
||||||
|
? profile.properties ?? {}
|
||||||
|
: {}),
|
||||||
|
...(payload.properties ?? {}),
|
||||||
|
};
|
||||||
|
await db.profile.update({
|
||||||
|
where: {
|
||||||
|
id: payload.profileId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
external_id: payload.id,
|
||||||
|
email: payload.email,
|
||||||
|
first_name: payload.first_name,
|
||||||
|
last_name: payload.last_name,
|
||||||
|
avatar: payload.avatar,
|
||||||
|
properties: profile.properties,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'set_profile_property': {
|
||||||
|
if (
|
||||||
|
typeof (profile.properties as Record<string, unknown>)[
|
||||||
|
payload.name
|
||||||
|
] === 'undefined'
|
||||||
|
) {
|
||||||
|
(profile.properties as Record<string, unknown>)[payload.name] =
|
||||||
|
payload.value;
|
||||||
|
|
||||||
|
await db.profile.update({
|
||||||
|
where: {
|
||||||
|
id: payload.profileId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
// @ts-expect-error
|
||||||
|
properties: profile.properties,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'increment': {
|
||||||
|
await tickProfileProperty({
|
||||||
|
profileId: payload.profileId,
|
||||||
|
name: payload.name,
|
||||||
|
tick: payload.value,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'decrement': {
|
||||||
|
await tickProfileProperty({
|
||||||
|
profileId: payload.profileId,
|
||||||
|
name: payload.name,
|
||||||
|
tick: -Math.abs(payload.value),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'event': {
|
||||||
|
await db.event.create({
|
||||||
|
data: {
|
||||||
|
name: payload.name,
|
||||||
|
properties: payload.properties,
|
||||||
|
createdAt: payload.time,
|
||||||
|
project_id: projectId,
|
||||||
|
profile_id: payload.profileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update_session': {
|
||||||
|
const session = await db.event.findFirst({
|
||||||
|
where: {
|
||||||
|
profile_id: payload.profileId,
|
||||||
|
project_id: projectId,
|
||||||
|
name: 'session_start',
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (session) {
|
||||||
|
await db.$executeRawUnsafe(
|
||||||
|
`UPDATE events SET properties = '${JSON.stringify(
|
||||||
|
payload.properties
|
||||||
|
)}' || properties WHERE "createdAt" >= '${session.createdAt.toISOString()}' AND profile_id = '${
|
||||||
|
payload.profileId
|
||||||
|
}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
job.log(`Failed to create "${item.type}"`);
|
||||||
|
job.log(` > Payload: ${JSON.stringify(item.payload)}`);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
job.log(` > Error: ${error.message.trim()}`);
|
||||||
|
job.log(` > Stack: ${error.stack}`);
|
||||||
|
}
|
||||||
|
failedEvents.push(item);
|
||||||
|
job.log(`---`);
|
||||||
|
}
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
await db.eventFailed.createMany({
|
||||||
|
data: failedEvents.map((item) => ({
|
||||||
|
data: item as Record<string, any>,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tickProfileProperty({
|
||||||
|
profileId,
|
||||||
|
tick,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
profileId: string;
|
||||||
|
tick: number;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
const profile = await db.profile.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: profileId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const properties = (
|
||||||
|
typeof profile.properties === 'object' ? profile.properties ?? {} : {}
|
||||||
|
) as Record<string, number>;
|
||||||
|
const value = name in properties ? properties[name] : 0;
|
||||||
|
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
return `Property "${name}" on user is of type ${typeof value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tick !== 'number') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
return `Value is not a number ${tick} (${typeof tick})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.profile.update({
|
||||||
|
where: {
|
||||||
|
id: profileId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
properties: {
|
||||||
|
...properties,
|
||||||
|
[name]: value + tick,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
12
apps/worker/tsconfig.json
Normal file
12
apps/worker/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@mixan/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
17
apps/worker/tsup.config.ts
Normal file
17
apps/worker/tsup.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
import type { Options } from 'tsup';
|
||||||
|
|
||||||
|
const options: Options = {
|
||||||
|
clean: true,
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
noExternal: [/^@mixan\/.*$/u, /^@\/.*$/u],
|
||||||
|
sourcemap: true,
|
||||||
|
splitting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.WATCH) {
|
||||||
|
options.watch = ['src/**/*', '../../packages/**/*'];
|
||||||
|
options.onSuccess = 'node dist/index.js';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig(options);
|
||||||
10
build_docker
10
build_docker
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--build-arg DATABASE_URL="postgresql://local@host.docker.internal:5432/mixan?schema=public" \
|
|
||||||
--build-arg NEXTAUTH_SECRET="secret_sauce" \
|
|
||||||
--build-arg NEXTAUTH_URL="http://localhost:3000" \
|
|
||||||
-t mixan/app:latest \
|
|
||||||
-t mixan/app:1.0 \
|
|
||||||
-f apps/web/Dockerfile \
|
|
||||||
.
|
|
||||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_USER: username
|
||||||
|
ports:
|
||||||
|
- 5435:5432
|
||||||
|
restart: on-failure:3
|
||||||
|
pgweb:
|
||||||
|
image: sosedoff/pgweb
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
PGWEB_DATABASE_URL: postgres://username:password@postgres:5432/postgres?sslmode=disable
|
||||||
|
ports:
|
||||||
|
- 8085:8081
|
||||||
|
restart: on-failure:3
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
command: redis-server
|
||||||
|
volumes:
|
||||||
|
- redis:/var/lib/redis
|
||||||
|
- redis-config:/usr/local/etc/redis/redis.conf
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
networks:
|
||||||
|
- redis-network
|
||||||
|
redis-commander:
|
||||||
|
image: rediscommander/redis-commander:latest
|
||||||
|
environment:
|
||||||
|
- REDIS_HOSTS=local:redis:6379
|
||||||
|
- HTTP_USER=root
|
||||||
|
- HTTP_PASSWORD=qwerty
|
||||||
|
ports:
|
||||||
|
- 8081:8081
|
||||||
|
networks:
|
||||||
|
- redis-network
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
redis:
|
||||||
|
redis-config:
|
||||||
|
networks:
|
||||||
|
redis-network:
|
||||||
|
driver: bridge
|
||||||
103
docker/Dockerfile-composed
Normal file
103
docker/Dockerfile-composed
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Docker file that contains all the necessary services to run the app
|
||||||
|
# EXCEPT the database. The database is run in a separate container.
|
||||||
|
|
||||||
|
# Services: web, worker (queue), redis
|
||||||
|
# Excluded: db (postgres)
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 node:20-slim AS base
|
||||||
|
|
||||||
|
ARG DATABASE_URL="postgresql://local@host.docker.internal:5432/mixan?schema=public"
|
||||||
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
|
||||||
|
ARG NEXTAUTH_SECRET="secret_sauce"
|
||||||
|
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||||
|
|
||||||
|
ARG NEXTAUTH_URL="http://localhost:3300"
|
||||||
|
ENV NEXTAUTH_URL=$NEXTAUTH_URL
|
||||||
|
|
||||||
|
ARG REDIS_URL="redis://127.0.0.1:6379"
|
||||||
|
ENV REDIS_URL=$REDIS_URL
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y curl supervisor redis \
|
||||||
|
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
|
||||||
|
&& bash n $NODE_VERSION \
|
||||||
|
&& rm n \
|
||||||
|
&& npm install -g n
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY pnpm-lock.yaml pnpm-lock.yaml
|
||||||
|
COPY pnpm-workspace.yaml pnpm-workspace.yaml
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY apps/worker/package.json apps/worker/package.json
|
||||||
|
COPY packages/db/package.json packages/db/package.json
|
||||||
|
COPY packages/queue/package.json packages/queue/package.json
|
||||||
|
COPY packages/types/package.json packages/types/package.json
|
||||||
|
|
||||||
|
# BUILD
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY apps apps
|
||||||
|
COPY packages packages
|
||||||
|
COPY tooling tooling
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# PROD
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||||
|
|
||||||
|
# FINAL
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
COPY --from=build /app/package.json /app/package.json
|
||||||
|
COPY --from=prod /app/node_modules /app/node_modules
|
||||||
|
# Apps
|
||||||
|
COPY --from=build /app/apps/web /app/apps/web
|
||||||
|
COPY --from=build /app/apps/worker /app/apps/worker
|
||||||
|
# Apps node_modules
|
||||||
|
COPY --from=prod /app/apps/web/node_modules /app/apps/web/node_modules
|
||||||
|
COPY --from=prod /app/apps/worker/node_modules /app/apps/worker/node_modules
|
||||||
|
# Packages
|
||||||
|
COPY --from=build /app/packages/db /app/packages/db
|
||||||
|
COPY --from=build /app/packages/queue /app/packages/queue
|
||||||
|
# Packages node_modules
|
||||||
|
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
|
||||||
|
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
|
||||||
|
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/supervisor /data
|
||||||
|
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY docker/redis.conf /etc/redis/redis.conf
|
||||||
|
|
||||||
|
# Redis data
|
||||||
|
VOLUME [ "/data" ]
|
||||||
|
|
||||||
|
EXPOSE 3000 3001
|
||||||
|
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
85
docker/Dockerfile-web
Normal file
85
docker/Dockerfile-web
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Dockerfile that builds the web app only
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 node:20-slim AS base
|
||||||
|
|
||||||
|
ARG DATABASE_URL="postgresql://local@host.docker.internal:5432/mixan?schema=public"
|
||||||
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
|
||||||
|
ARG NEXTAUTH_SECRET="secret_sauce"
|
||||||
|
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||||
|
|
||||||
|
ARG NEXTAUTH_URL="http://localhost:3300"
|
||||||
|
ENV NEXTAUTH_URL=$NEXTAUTH_URL
|
||||||
|
|
||||||
|
ARG REDIS_URL="redis://127.0.0.1:6379"
|
||||||
|
ENV REDIS_URL=$REDIS_URL
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y curl \
|
||||||
|
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
|
||||||
|
&& bash n $NODE_VERSION \
|
||||||
|
&& rm n \
|
||||||
|
&& npm install -g n
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY pnpm-lock.yaml pnpm-lock.yaml
|
||||||
|
COPY pnpm-workspace.yaml pnpm-workspace.yaml
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY packages/db/package.json packages/db/package.json
|
||||||
|
COPY packages/queue/package.json packages/queue/package.json
|
||||||
|
COPY packages/types/package.json packages/types/package.json
|
||||||
|
|
||||||
|
# BUILD
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY apps apps
|
||||||
|
COPY packages packages
|
||||||
|
COPY tooling tooling
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# PROD
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||||
|
|
||||||
|
# FINAL
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
COPY --from=build /app/package.json /app/package.json
|
||||||
|
COPY --from=prod /app/node_modules /app/node_modules
|
||||||
|
# Apps
|
||||||
|
COPY --from=build /app/apps/web /app/apps/web
|
||||||
|
# Apps node_modules
|
||||||
|
COPY --from=prod /app/apps/web/node_modules /app/apps/web/node_modules
|
||||||
|
# Packages
|
||||||
|
COPY --from=build /app/packages/db /app/packages/db
|
||||||
|
COPY --from=build /app/packages/queue /app/packages/queue
|
||||||
|
# Packages node_modules
|
||||||
|
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
|
||||||
|
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
|
||||||
|
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
WORKDIR /app/apps/web
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
79
docker/Dockerfile-worker
Normal file
79
docker/Dockerfile-worker
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Dockerfile that builds the worker only
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 node:20-slim AS base
|
||||||
|
|
||||||
|
ARG DATABASE_URL="postgresql://local@host.docker.internal:5432/mixan?schema=public"
|
||||||
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
|
||||||
|
ARG REDIS_URL="redis://127.0.0.1:6379"
|
||||||
|
ENV REDIS_URL=$REDIS_URL
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y curl \
|
||||||
|
&& curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
|
||||||
|
&& bash n $NODE_VERSION \
|
||||||
|
&& rm n \
|
||||||
|
&& npm install -g n
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY pnpm-lock.yaml pnpm-lock.yaml
|
||||||
|
COPY pnpm-workspace.yaml pnpm-workspace.yaml
|
||||||
|
COPY apps/worker/package.json apps/worker/package.json
|
||||||
|
COPY packages/db/package.json packages/db/package.json
|
||||||
|
COPY packages/queue/package.json packages/queue/package.json
|
||||||
|
COPY packages/types/package.json packages/types/package.json
|
||||||
|
|
||||||
|
# BUILD
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY apps apps
|
||||||
|
COPY packages packages
|
||||||
|
COPY tooling tooling
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# PROD
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||||
|
|
||||||
|
# FINAL
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
COPY --from=build /app/package.json /app/package.json
|
||||||
|
COPY --from=prod /app/node_modules /app/node_modules
|
||||||
|
# Apps
|
||||||
|
COPY --from=build /app/apps/worker /app/apps/worker
|
||||||
|
# Apps node_modules
|
||||||
|
COPY --from=prod /app/apps/worker/node_modules /app/apps/worker/node_modules
|
||||||
|
# Packages
|
||||||
|
COPY --from=build /app/packages/db /app/packages/db
|
||||||
|
COPY --from=build /app/packages/queue /app/packages/queue
|
||||||
|
# Packages node_modules
|
||||||
|
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
|
||||||
|
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
|
||||||
|
|
||||||
|
RUN pnpm db:codegen
|
||||||
|
|
||||||
|
WORKDIR /app/apps/worker
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
11
docker/build-composed
Executable file
11
docker/build-composed
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE_URL="postgresql://local@host.docker.internal:5432/mixan?schema=public" \
|
||||||
|
--build-arg NEXTAUTH_SECRET="secret_sauce" \
|
||||||
|
--build-arg NEXTAUTH_URL="http://localhost:3300" \
|
||||||
|
--build-arg REDIS_URL="redis://127.0.0.1:6379" \
|
||||||
|
-t mixan/composed:latest \
|
||||||
|
-t mixan/composed:1.0 \
|
||||||
|
-f docker/Dockerfile-composed \
|
||||||
|
.
|
||||||
1329
docker/redis.conf
Normal file
1329
docker/redis.conf
Normal file
File diff suppressed because it is too large
Load Diff
34
docker/supervisord.conf
Normal file
34
docker/supervisord.conf
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
|
||||||
|
[include]
|
||||||
|
files = /etc/supervisor/conf.d/*.conf
|
||||||
|
|
||||||
|
[program:web]
|
||||||
|
directory=/app/apps/web
|
||||||
|
command=pnpm start
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
redirect_stderr=true
|
||||||
|
|
||||||
|
[program:worker]
|
||||||
|
directory=/app/apps/worker
|
||||||
|
command=pnpm start
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
redirect_stderr=true
|
||||||
|
|
||||||
|
[program:redis]
|
||||||
|
command=redis-server /etc/redis/redis.conf
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
user=root
|
||||||
|
stdout_logfile=/var/log/redis/stdout.log
|
||||||
|
stderr_logfile=/var/log/redis/stderr.log
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
"packageManager": "pnpm@8.7.6",
|
"packageManager": "pnpm@8.7.6",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"db:codegen": "pnpm -r --filter db run codegen",
|
||||||
|
"migrate": "pnpm -r --filter db run migrate",
|
||||||
|
"migrate:deploy": "pnpm -r --filter db run migrate:deploy",
|
||||||
"dev": "pnpm -r dev",
|
"dev": "pnpm -r dev",
|
||||||
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||||
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||||
@@ -16,11 +19,10 @@
|
|||||||
"typecheck": "pnpm -r typecheck"
|
"typecheck": "pnpm -r typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dotenv-cli": "^7.3.0",
|
||||||
"@mixan/prettier-config": "^0.1.0",
|
"@mixan/prettier-config": "^0.1.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"semver": "^7.5.4"
|
"semver": "^7.5.4"
|
||||||
},
|
},
|
||||||
"prettier": "@mixan/prettier-config"
|
"prettier": "@mixan/prettier-config"
|
||||||
|
|||||||
15
packages/db/index.ts
Normal file
15
packages/db/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export * from '@prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const db =
|
||||||
|
globalForPrisma.prisma ??
|
||||||
|
new PrismaClient({
|
||||||
|
log: ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||||
35
packages/db/package.json
Normal file
35
packages/db/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@mixan/db",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"codegen": "pnpm with-env prisma generate",
|
||||||
|
"migrate": "pnpm with-env prisma migrate dev",
|
||||||
|
"migrate:deploy": "pnpm with-env prisma migrate deploy",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "dotenv -e ../../.env -c --"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^5.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mixan/eslint-config": "workspace:*",
|
||||||
|
"@mixan/prettier-config": "workspace:*",
|
||||||
|
"@mixan/tsconfig": "workspace:*",
|
||||||
|
"@mixan/types": "workspace:*",
|
||||||
|
"@types/node": "^18.16.0",
|
||||||
|
"eslint": "^8.48.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"prisma": "^5.1.1",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"@mixan/eslint-config/base"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@mixan/prettier-config"
|
||||||
|
}
|
||||||
12
packages/db/tsconfig.json
Normal file
12
packages/db/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@mixan/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
2
packages/queue/index.ts
Normal file
2
packages/queue/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { eventsQueue } from './src/queues';
|
||||||
|
export { connection } from './src/connection';
|
||||||
30
packages/queue/package.json
Normal file
30
packages/queue/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@mixan/queue",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bullmq": "^5.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mixan/eslint-config": "workspace:*",
|
||||||
|
"@mixan/prettier-config": "workspace:*",
|
||||||
|
"@mixan/tsconfig": "workspace:*",
|
||||||
|
"@mixan/types": "workspace:*",
|
||||||
|
"@types/node": "^18.16.0",
|
||||||
|
"eslint": "^8.48.0",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"@mixan/eslint-config/base"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": "@mixan/prettier-config"
|
||||||
|
}
|
||||||
10
packages/queue/src/connection.ts
Normal file
10
packages/queue/src/connection.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const parse = (connectionString: string) => {
|
||||||
|
const url = new URL(connectionString);
|
||||||
|
return {
|
||||||
|
host: url.hostname,
|
||||||
|
port: Number(url.port),
|
||||||
|
password: url.password,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connection = parse(String(process.env.REDIS_URL));
|
||||||
17
packages/queue/src/queues.ts
Normal file
17
packages/queue/src/queues.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Queue } from 'bullmq';
|
||||||
|
|
||||||
|
import type { BatchPayload } from '@mixan/types';
|
||||||
|
|
||||||
|
import { connection } from './connection';
|
||||||
|
|
||||||
|
export interface EventsQueuePayload {
|
||||||
|
projectId: string;
|
||||||
|
payload: BatchPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventsQueue = new Queue<EventsQueuePayload>('events', {
|
||||||
|
connection,
|
||||||
|
defaultJobOptions: {
|
||||||
|
removeOnComplete: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
12
packages/queue/tsconfig.json
Normal file
12
packages/queue/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@mixan/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ export function getDevice() {
|
|||||||
ua
|
ua
|
||||||
);
|
);
|
||||||
const t2 =
|
const t2 =
|
||||||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
|
||||||
ua.slice(0, 4)
|
ua.slice(0, 4)
|
||||||
);
|
);
|
||||||
if (t1 || t2) {
|
if (t1 || t2) {
|
||||||
|
|||||||
831
pnpm-lock.yaml
generated
831
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user