Compare commits
20 Commits
6215e21bed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1117b4adc0 | |||
| 8de1f87435 | |||
| d82416e4a3 | |||
| 926fc0fd0b | |||
| 0370930894 | |||
| 8831857007 | |||
| 095463cafa | |||
| 742242ffb8 | |||
| 61de53c014 | |||
| cda02e35fb | |||
| 41c71a9877 | |||
| d8be1c0a69 | |||
| 1b31f3194d | |||
| 27b7d87e68 | |||
| 01e346e157 | |||
| 1df8a3f452 | |||
| aad888ff2c | |||
| 906c9765b5 | |||
| 559a7d9a21 | |||
| 284c792a92 |
17
.env.example
17
.env.example
@@ -1,9 +1,8 @@
|
|||||||
NUXT_OAUTH_STRAVA_CLIENT_ID=[YOUR_STRAVA_CLIENT_ID]
|
NUXT_OAUTH_STRAVA_CLIENT_ID=your_client_id
|
||||||
NUXT_OAUTH_STRAVA_CLIENT_SECRET=[YOUR_STRAVA_CLIENT_SECRET]
|
NUXT_OAUTH_STRAVA_CLIENT_SECRET=your_client_secret
|
||||||
NUXT_SESSION_PASSWORD=[YOUR_SESSION_PASSWORD]
|
NUXT_SESSION_PASSWORD=your_session_password_min_32_chars
|
||||||
NUXT_STRAVA_VERIFY_TOKEN=[YOUR_STRAVA_VERIFY_TOKEN]
|
NUXT_STRAVA_VERIFY_TOKEN=your_verify_token
|
||||||
NUXT_OPENAI_API_KEY=[YOUR_OPENAI_API_KEY]
|
NUXT_OPENAI_API_KEY=your_openai_api_key
|
||||||
NUXT_PUBLIC_APTABASE_APP_KEY=[YOUR_APTABASE_APP_KEY]
|
NUXT_DATABASE_URL=file:./tmp/ghostwriter.db
|
||||||
NUXT_WEBHOOKS_URL=[YOUR_WEBHOOKS_URL]
|
NUXT_HOOKDECK_KEY=your_hookdeck_key
|
||||||
NUXT_DATABASE_URL=[YOUR_DATABASE_URL]
|
NUXT_OPENROUTER_API_KEY=your_openrouter_api_key
|
||||||
NUXT_HOOKDECK_KEY=[YOUR_HOOKDECK_KEY]
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ logs
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
tmp/
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -2,24 +2,47 @@
|
|||||||
|
|
||||||
## Strava Activity Title & Description Generator
|
## Strava Activity Title & Description Generator
|
||||||
|
|
||||||
Ghostwriter is a Nuxt-based application that helps athletes generate fun and creative titles and descriptions for your Strava activities.
|
Ghostwriter is a Nuxt app that conjures up the most hilarious, epic, and mildly unhinged titles and descriptions for your Strava activities. Because let's be honest, "Morning Run" doesn't quite capture the chaos of dodging dogs at 6 AM.
|
||||||
|
|
||||||
## Installation
|
Built with Nuxt 4, Bun (it's fast, duh), Drizzle ORM, and @nuxt/ui. Powers through your Strava data like a caffeinated marathoner.
|
||||||
|
|
||||||
1. Clone the repository:
|
## Quick Start
|
||||||
|
|
||||||
|
1. Copy the example env file:
|
||||||
```bash
|
```bash
|
||||||
git clone [repository_url]
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
2. Navigate to the project directory:
|
|
||||||
|
2. Install and run:
|
||||||
```bash
|
```bash
|
||||||
cd ghostwriter
|
bun install
|
||||||
|
bun run dev
|
||||||
```
|
```
|
||||||
3. Install dependencies:
|
|
||||||
```bash
|
Open http://localhost:3000 and start ghostwriting those activities into legend.
|
||||||
npm install
|
|
||||||
```
|
## Commands
|
||||||
4. Run the development server:
|
|
||||||
```bash
|
| Command | What it does |
|
||||||
npm run dev
|
|---------|--------------|
|
||||||
```
|
| `bun run dev` | Spin up the dev server |
|
||||||
5. Access the application at http://localhost:3000
|
| `bun run build` | Build for production |
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Nuxt 4** — The skeleton
|
||||||
|
- **Bun** — Runtime so fast it's basically cheating
|
||||||
|
- **Drizzle ORM** — Database wizardry
|
||||||
|
- **@nuxt/ui v3** — Beautiful UI components
|
||||||
|
- **SQLite** — Your data's cozy little home
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork it
|
||||||
|
2. Create a branch (`git checkout -b fix-that-bug-i-guess`)
|
||||||
|
3. Commit your changes
|
||||||
|
4. Push and open a PR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ghostwriter: Making your Strava activities slightly less embarrassing since 2025.*
|
||||||
|
|||||||
28
agents.md
Normal file
28
agents.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Agents
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
pnpm run dev
|
||||||
|
|
||||||
|
# Build
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# Generate static site
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
pnpm exec vue-tsc --noEmit
|
||||||
|
|
||||||
|
# Database
|
||||||
|
pnpm exec drizzle-kit push
|
||||||
|
pnpm exec drizzle-kit studio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Info
|
||||||
|
|
||||||
|
- Nuxt 4
|
||||||
|
- Uses pnpm as package manager and runtime
|
||||||
|
- Database: Drizzle ORM with sqlite
|
||||||
|
- UI: @nuxt/ui v4
|
||||||
@@ -13,7 +13,9 @@ const { openInPopup } = useUserSession();
|
|||||||
<UCard class="max-w-sm grid gap-6 justify-center items-center">
|
<UCard class="max-w-sm grid gap-6 justify-center items-center">
|
||||||
<div class="flex flex-col gap-10 items-center justify-center">
|
<div class="flex flex-col gap-10 items-center justify-center">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="font-bold text-xl tracking-tight font-fira-code">
|
<div
|
||||||
|
class="font-bold text-xl tracking-tight font-fira-code"
|
||||||
|
>
|
||||||
Ghostwriter
|
Ghostwriter
|
||||||
</div>
|
</div>
|
||||||
<NuxtImg src="/ghostwriter-logo.png" class="size-9" />
|
<NuxtImg src="/ghostwriter-logo.png" class="size-9" />
|
||||||
@@ -23,8 +25,8 @@ const { openInPopup } = useUserSession();
|
|||||||
Sign in to your account.
|
Sign in to your account.
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
Connect with Strava to automatically add personalized titles and
|
Connect with Strava to automatically add personalized
|
||||||
descriptions to your activities.
|
titles and descriptions to your activities.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig } from "drizzle-kit";
|
|||||||
import { get } from "radash";
|
import { get } from "radash";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "sqlite",
|
||||||
schema: "./server/database/schema.ts",
|
schema: "./server/database/schema.ts",
|
||||||
out: "./server/database/migrations",
|
out: "./server/database/migrations",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
|
|||||||
@@ -12,13 +12,8 @@ export default defineNuxtConfig({
|
|||||||
webhooksUrl: "",
|
webhooksUrl: "",
|
||||||
stravaVerifyToken: "",
|
stravaVerifyToken: "",
|
||||||
hookdeckKey: "",
|
hookdeckKey: "",
|
||||||
openaiApiKey: "",
|
openrouterApiKey: "",
|
||||||
databaseUrl: "",
|
databaseUrl: "",
|
||||||
public: {
|
|
||||||
posthogPublicKey: "",
|
|
||||||
posthogHost: "",
|
|
||||||
posthogDefaults: "2025-05-24",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
future: { compatibilityVersion: 4 },
|
future: { compatibilityVersion: 4 },
|
||||||
compatibilityDate: "2025-03-01",
|
compatibilityDate: "2025-03-01",
|
||||||
@@ -55,4 +50,10 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
nitro: {
|
||||||
|
preset: "node-server",
|
||||||
|
experimental: {
|
||||||
|
tasks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
16152
package-lock.json
generated
16152
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@@ -6,42 +6,49 @@
|
|||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev --host=0.0.0.0",
|
"dev": "nuxt dev --host=0.0.0.0",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"start": "node .output/server/index.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/tempo": "^0.1.2",
|
"@formkit/tempo": "^1.0.0",
|
||||||
"@google/genai": "^1.13.0",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@neondatabase/serverless": "^1.0.1",
|
"@nuxt/image": "^2.0.0",
|
||||||
"@nuxt/icon": "1.15.0",
|
"@nuxt/ui": "4.5.1",
|
||||||
"@nuxt/image": "^1.11.0",
|
|
||||||
"@nuxt/ui": "3.3.0",
|
|
||||||
"@vee-validate/nuxt": "^4.15.1",
|
"@vee-validate/nuxt": "^4.15.1",
|
||||||
"@vercel/functions": "^2.2.8",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"@vueuse/nuxt": "^13.6.0",
|
"better-sqlite3": "^12.6.2",
|
||||||
"destr": "^2.0.5",
|
"destr": "^2.0.5",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.4",
|
||||||
"nuxt": "^4.0.3",
|
"nuxt": "^4.3.1",
|
||||||
"nuxt-auth-utils": "0.5.23",
|
"nuxt-auth-utils": "0.5.29",
|
||||||
"openai": "^5.12.2",
|
"openai": "^6.27.0",
|
||||||
"posthog-js": "^1.259.0",
|
|
||||||
"posthog-node": "^5.6.0",
|
|
||||||
"radash": "^12.1.1",
|
"radash": "^12.1.1",
|
||||||
"ts-pattern": "^5.8.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"url": "^0.11.4",
|
"url": "^0.11.4",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.29",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^5.0.3",
|
||||||
"zod": "^4.0.16"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/heroicons": "^1.2.3",
|
||||||
|
"@iconify-json/lucide": "^1.2.95",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/node": "^25.3.5",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vue-tsc": "^3.0.5"
|
"vue-tsc": "^3.2.5"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
"better-sqlite3",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"sharp",
|
"sharp",
|
||||||
"vue-demi",
|
"vue-demi",
|
||||||
"workerd"
|
"workerd"
|
||||||
|
],
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"better-sqlite3"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ const saveOp = watchPausable(
|
|||||||
<div class="font-bold text-lg">Welcome to Ghostwriter!</div>
|
<div class="font-bold text-lg">Welcome to Ghostwriter!</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Let's generate fun and engaging titles and descriptions for your Strava
|
Let's generate fun and engaging titles and descriptions for your
|
||||||
activities automatically, right when they are created. Customize your
|
Strava activities automatically, right when they are created.
|
||||||
preferences below.
|
Customize your preferences below.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Add a touch of creativity to your Strava workouts. Simply enable it and
|
Add a touch of creativity to your Strava workouts. Simply enable it
|
||||||
choose your language, and we'll do the rest!
|
and choose your language, and we'll do the rest!
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
|
|
||||||
@@ -69,12 +69,15 @@ const saveOp = watchPausable(
|
|||||||
<div class="font-bold text-lg">❤️ Support</div>
|
<div class="font-bold text-lg">❤️ Support</div>
|
||||||
<UCard class="">
|
<UCard class="">
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
Ghostwriter 👻 is free to use, but it takes time and resources to keep
|
Ghostwriter 👻 is free to use, but it takes time and resources
|
||||||
it running smoothly. If you enjoy it, consider supporting the app and
|
to keep it running smoothly. If you enjoy it, consider
|
||||||
its creator - every bit helps!
|
supporting the app and its creator - every bit helps!
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ULink href="https://buymeacoffee.com/mariosant" target="_blank">
|
<ULink
|
||||||
|
href="https://buymeacoffee.com/mariosant"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
src="images/bmac-orange-button.png"
|
src="images/bmac-orange-button.png"
|
||||||
height="32px"
|
height="32px"
|
||||||
@@ -190,11 +193,15 @@ const saveOp = watchPausable(
|
|||||||
<CardField>
|
<CardField>
|
||||||
<template #title> Athlete ID </template>
|
<template #title> Athlete ID </template>
|
||||||
<template #description>
|
<template #description>
|
||||||
Your Athlete ID. Click it to view your profile on Strava.</template
|
Your Athlete ID. Click it to view your profile on
|
||||||
|
Strava.</template
|
||||||
>
|
>
|
||||||
|
|
||||||
<template #value>
|
<template #value>
|
||||||
<ULink :href="stravaLink" class="underline flex items-center gap-2">
|
<ULink
|
||||||
|
:href="stravaLink"
|
||||||
|
class="underline flex items-center gap-2"
|
||||||
|
>
|
||||||
{{ user.id }}
|
{{ user.id }}
|
||||||
<UIcon
|
<UIcon
|
||||||
name="heroicons:arrow-top-right-on-square"
|
name="heroicons:arrow-top-right-on-square"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import posthog from "posthog-js";
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
|
|
||||||
const posthogClient = posthog.init(runtimeConfig.public.posthogPublicKey, {
|
|
||||||
api_host: runtimeConfig.public.posthogHost,
|
|
||||||
//@ts-expect-error typing is more explicit than what it should
|
|
||||||
defaults: runtimeConfig.public.posthogDefaults,
|
|
||||||
loaded: (posthog) => {
|
|
||||||
if (import.meta.env.MODE === "development") posthog.debug();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
posthog: () => posthogClient,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
9111
pnpm-lock.yaml
generated
9111
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
onlyBuiltDependencies:
|
|
||||||
- '@parcel/watcher'
|
|
||||||
- '@tailwindcss/oxide'
|
|
||||||
- core-js
|
|
||||||
- esbuild
|
|
||||||
- sharp
|
|
||||||
- vue-demi
|
|
||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
availableTones,
|
availableTones,
|
||||||
availableUnits,
|
availableUnits,
|
||||||
} from "~/shared/constants";
|
} from "~/shared/constants";
|
||||||
//
|
|
||||||
const bodySchema = z.strictObject({
|
const bodySchema = z.strictObject({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
language: z.enum(availableLanguages),
|
language: z.enum(availableLanguages),
|
||||||
units: z.enum(availableUnits),
|
units: z.enum(availableUnits).default(availableUnits[1]),
|
||||||
tone: z.array(z.enum(availableTones)),
|
tone: z.array(z.enum(availableTones)).default([]),
|
||||||
highlights: z.array(z.enum(availableHighlights)),
|
highlights: z.array(z.enum(availableHighlights)).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { get } from "radash";
|
|
||||||
import { createActivityContent } from "~~/server/utils/create-content";
|
import { createActivityContent } from "~~/server/utils/create-content";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await validateHookdeck(event);
|
await validateHookdeck(event);
|
||||||
|
|
||||||
const posthog = event.context.posthog;
|
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const db = useDrizzle();
|
const db = useDrizzle();
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const currentActivity = await strava!<any>(`/activities/${body.object_id}`);
|
const currentActivity = await strava!<any>(`/activities/${body.object_id}`);
|
||||||
const [, ...previousActivities] = await strava!<any[]>(`/activities`, {
|
const [, ...previousActivities] = await strava!<any[]>(`/activities`, {
|
||||||
query: {
|
query: {
|
||||||
per_page: 20,
|
per_page: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,8 +41,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
await strava!(`activities/${body.object_id}`, {
|
await strava!(`activities/${body.object_id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: {
|
body: {
|
||||||
name: stravaRequestBody.name,
|
name: (stravaRequestBody.name as String).replaceAll("—", ","),
|
||||||
description: stravaRequestBody.description,
|
description: (stravaRequestBody.description as String).replaceAll(
|
||||||
|
"—",
|
||||||
|
",",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -53,15 +53,4 @@ export default defineEventHandler(async (event) => {
|
|||||||
message: `Strava API: ${error.message}`,
|
message: `Strava API: ${error.message}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
posthog.captureImmediate({
|
|
||||||
distinctId: String(user.id),
|
|
||||||
event: "content generated",
|
|
||||||
properties: {
|
|
||||||
activity: currentActivity.id,
|
|
||||||
activityType: get(currentActivity, "sport_type", "unknown"),
|
|
||||||
highlight: stravaRequestBody.meta.highlight,
|
|
||||||
tone: stravaRequestBody.meta.tone,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { eq } from "drizzle-orm";
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
await validateHookdeck(event);
|
await validateHookdeck(event);
|
||||||
|
|
||||||
const posthog = event.context.posthog;
|
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const db = useDrizzle();
|
const db = useDrizzle();
|
||||||
|
|
||||||
@@ -13,29 +11,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: (f, o) => o.eq(f.id, get(body, "object_id")),
|
|
||||||
with: {
|
|
||||||
preferences: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog.identifyImmediate({
|
|
||||||
distinctId: String(user!.id),
|
|
||||||
properties: {
|
|
||||||
name: user!.name,
|
|
||||||
country: user!.country,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.delete(tables.users)
|
.delete(tables.users)
|
||||||
.where(eq(tables.users.id, get(body, "object_id")));
|
.where(eq(tables.users.id, get(body, "object_id")));
|
||||||
|
|
||||||
posthog.captureImmediate({
|
|
||||||
distinctId: get(body, "object_id"),
|
|
||||||
event: "user deleted",
|
|
||||||
});
|
|
||||||
|
|
||||||
sendNoContent(event);
|
sendNoContent(event);
|
||||||
});
|
});
|
||||||
|
|||||||
28
server/database/migrations/0000_groovy_rachel_grey.sql
Normal file
28
server/database/migrations/0000_groovy_rachel_grey.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TABLE `preferences` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`user_id` numeric,
|
||||||
|
`data` text,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `preferences_user_id_unique` ON `preferences` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `tokens` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`user_id` numeric,
|
||||||
|
`refresh_token` text,
|
||||||
|
`access_token` text,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `tokens_user_id_unique` ON `tokens` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` numeric PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`avatar` text,
|
||||||
|
`city` text,
|
||||||
|
`country` text,
|
||||||
|
`sex` text,
|
||||||
|
`weight` integer,
|
||||||
|
`created_at` integer
|
||||||
|
);
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
CREATE TABLE "preferences" (
|
|
||||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "preferences_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
|
||||||
"user_id" integer,
|
|
||||||
"data" jsonb,
|
|
||||||
CONSTRAINT "preferences_user_id_unique" UNIQUE("user_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "tokens" (
|
|
||||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "tokens_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
|
||||||
"user_id" integer,
|
|
||||||
"refresh_token" text,
|
|
||||||
"access_token" text,
|
|
||||||
"expires_at" timestamp DEFAULT now() NOT NULL,
|
|
||||||
CONSTRAINT "tokens_user_id_unique" UNIQUE("user_id")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "users" (
|
|
||||||
"id" integer PRIMARY KEY NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"avatar" text NOT NULL,
|
|
||||||
"city" text,
|
|
||||||
"country" text,
|
|
||||||
"sex" text,
|
|
||||||
"weight" numeric,
|
|
||||||
"created_at" timestamp DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "preferences" ADD CONSTRAINT "preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "tokens" ADD CONSTRAINT "tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "users" ADD COLUMN "premium" boolean;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "users" ALTER COLUMN "premium" SET NOT NULL;
|
|
||||||
@@ -1,210 +1,204 @@
|
|||||||
{
|
{
|
||||||
"id": "c8519a52-b999-48f3-b532-42a5678e3905",
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "347ea848-a216-4fff-910a-90ba2f9aa91c",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
"tables": {
|
||||||
"public.preferences": {
|
"preferences": {
|
||||||
"name": "preferences",
|
"name": "preferences",
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"identity": {
|
"autoincrement": true
|
||||||
"type": "always",
|
|
||||||
"name": "preferences_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"user_id": {
|
"user_id": {
|
||||||
"name": "user_id",
|
"name": "user_id",
|
||||||
"type": "integer",
|
"type": "numeric",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"name": "data",
|
"name": "data",
|
||||||
"type": "jsonb",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"preferences_user_id_unique": {
|
||||||
|
"name": "preferences_user_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"preferences_user_id_users_id_fk": {
|
"preferences_user_id_users_id_fk": {
|
||||||
"name": "preferences_user_id_users_id_fk",
|
"name": "preferences_user_id_users_id_fk",
|
||||||
"tableFrom": "preferences",
|
"tableFrom": "preferences",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": ["user_id"],
|
"columnsFrom": [
|
||||||
"columnsTo": ["id"],
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"preferences_user_id_unique": {
|
"checkConstraints": {}
|
||||||
"name": "preferences_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"policies": {},
|
"tokens": {
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.tokens": {
|
|
||||||
"name": "tokens",
|
"name": "tokens",
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"identity": {
|
"autoincrement": true
|
||||||
"type": "always",
|
|
||||||
"name": "tokens_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"user_id": {
|
"user_id": {
|
||||||
"name": "user_id",
|
"name": "user_id",
|
||||||
"type": "integer",
|
"type": "numeric",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"refresh_token": {
|
"refresh_token": {
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"access_token": {
|
"access_token": {
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"name": "expires_at",
|
"name": "expires_at",
|
||||||
"type": "timestamp",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "now()"
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"tokens_user_id_unique": {
|
||||||
|
"name": "tokens_user_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tokens_user_id_users_id_fk": {
|
"tokens_user_id_users_id_fk": {
|
||||||
"name": "tokens_user_id_users_id_fk",
|
"name": "tokens_user_id_users_id_fk",
|
||||||
"tableFrom": "tokens",
|
"tableFrom": "tokens",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": ["user_id"],
|
"columnsFrom": [
|
||||||
"columnsTo": ["id"],
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"tokens_user_id_unique": {
|
"checkConstraints": {}
|
||||||
"name": "tokens_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"policies": {},
|
"users": {
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "integer",
|
"type": "numeric",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"name": "avatar",
|
"name": "avatar",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"city": {
|
"city": {
|
||||||
"name": "city",
|
"name": "city",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"name": "country",
|
"name": "country",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"sex": {
|
"sex": {
|
||||||
"name": "sex",
|
"name": "sex",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"weight": {
|
"weight": {
|
||||||
"name": "weight",
|
"name": "weight",
|
||||||
"type": "numeric",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": false,
|
||||||
"default": "now()"
|
"autoincrement": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"policies": {},
|
"checkConstraints": {}
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
"tables": {}
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "912c6e36-57b1-4e7f-a29b-586b187b1c32",
|
|
||||||
"prevId": "c8519a52-b999-48f3-b532-42a5678e3905",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.preferences": {
|
|
||||||
"name": "preferences",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"identity": {
|
|
||||||
"type": "always",
|
|
||||||
"name": "preferences_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"name": "data",
|
|
||||||
"type": "jsonb",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"preferences_user_id_users_id_fk": {
|
|
||||||
"name": "preferences_user_id_users_id_fk",
|
|
||||||
"tableFrom": "preferences",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"preferences_user_id_unique": {
|
|
||||||
"name": "preferences_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.tokens": {
|
|
||||||
"name": "tokens",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"identity": {
|
|
||||||
"type": "always",
|
|
||||||
"name": "tokens_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"tokens_user_id_users_id_fk": {
|
|
||||||
"name": "tokens_user_id_users_id_fk",
|
|
||||||
"tableFrom": "tokens",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"tokens_user_id_unique": {
|
|
||||||
"name": "tokens_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"city": {
|
|
||||||
"name": "city",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"name": "country",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"sex": {
|
|
||||||
"name": "sex",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"premium": {
|
|
||||||
"name": "premium",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"weight": {
|
|
||||||
"name": "weight",
|
|
||||||
"type": "numeric",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "7b607167-551b-4aa3-8305-afe3c4d72ace",
|
|
||||||
"prevId": "912c6e36-57b1-4e7f-a29b-586b187b1c32",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.preferences": {
|
|
||||||
"name": "preferences",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"identity": {
|
|
||||||
"type": "always",
|
|
||||||
"name": "preferences_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"name": "data",
|
|
||||||
"type": "jsonb",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"preferences_user_id_users_id_fk": {
|
|
||||||
"name": "preferences_user_id_users_id_fk",
|
|
||||||
"tableFrom": "preferences",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"preferences_user_id_unique": {
|
|
||||||
"name": "preferences_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.tokens": {
|
|
||||||
"name": "tokens",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"identity": {
|
|
||||||
"type": "always",
|
|
||||||
"name": "tokens_id_seq",
|
|
||||||
"schema": "public",
|
|
||||||
"increment": "1",
|
|
||||||
"startWith": "1",
|
|
||||||
"minValue": "1",
|
|
||||||
"maxValue": "2147483647",
|
|
||||||
"cache": "1",
|
|
||||||
"cycle": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"tokens_user_id_users_id_fk": {
|
|
||||||
"name": "tokens_user_id_users_id_fk",
|
|
||||||
"tableFrom": "tokens",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"tokens_user_id_unique": {
|
|
||||||
"name": "tokens_user_id_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["user_id"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"avatar": {
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"city": {
|
|
||||||
"name": "city",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"name": "country",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"sex": {
|
|
||||||
"name": "sex",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"premium": {
|
|
||||||
"name": "premium",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"weight": {
|
|
||||||
"name": "weight",
|
|
||||||
"type": "numeric",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "sqlite",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "6",
|
||||||
"when": 1745335305323,
|
"when": 1772879556762,
|
||||||
"tag": "0000_slim_blonde_phantom",
|
"tag": "0000_groovy_rachel_grey",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1747547391071,
|
|
||||||
"tag": "0001_smooth_jazinda",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1747564780270,
|
|
||||||
"tag": "0002_many_marauders",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import { sqliteTable, text, integer, numeric } from "drizzle-orm/sqlite-core";
|
||||||
pgTable,
|
|
||||||
text,
|
|
||||||
integer,
|
|
||||||
numeric,
|
|
||||||
timestamp,
|
|
||||||
jsonb,
|
|
||||||
} from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
export const users = pgTable("users", {
|
export const users = sqliteTable("users", {
|
||||||
id: integer("id").primaryKey(),
|
id: numeric("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
avatar: text("avatar").notNull(),
|
avatar: text("avatar"),
|
||||||
city: text("city"),
|
city: text("city"),
|
||||||
country: text("country"),
|
country: text("country"),
|
||||||
sex: text("sex"),
|
sex: text("sex"),
|
||||||
weight: numeric("weight", {
|
weight: integer("weight"),
|
||||||
mode: "number",
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
||||||
}),
|
() => new Date(),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const preferences = pgTable("preferences", {
|
export const preferences = sqliteTable("preferences", {
|
||||||
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
userId: integer("user_id")
|
userId: numeric("user_id")
|
||||||
.references(() => users.id, {
|
.references(() => users.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
})
|
})
|
||||||
.unique(),
|
.unique(),
|
||||||
data: jsonb("data")
|
data: text("data", { mode: "json" })
|
||||||
.$type<{
|
.$type<{
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -45,19 +38,20 @@ export const preferences = pgTable("preferences", {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tokens = pgTable("tokens", {
|
export const tokens = sqliteTable("tokens", {
|
||||||
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
userId: integer("user_id")
|
userId: numeric("user_id")
|
||||||
.references(() => users.id, {
|
.references(() => users.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
})
|
})
|
||||||
.unique(),
|
.unique(),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text("refresh_token"),
|
||||||
accessToken: text("access_token"),
|
accessToken: text("access_token"),
|
||||||
expiresAt: timestamp("expires_at").notNull().defaultNow(),
|
expiresAt: integer("expires_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define relationships
|
|
||||||
export const usersRelations = relations(users, ({ one }) => ({
|
export const usersRelations = relations(users, ({ one }) => ({
|
||||||
tokens: one(tokens, {
|
tokens: one(tokens, {
|
||||||
fields: [users.id],
|
fields: [users.id],
|
||||||
@@ -69,7 +63,7 @@ export const usersRelations = relations(users, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const referencesRelations = relations(preferences, ({ one }) => ({
|
export const preferencesRelations = relations(preferences, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
fields: [preferences.userId],
|
fields: [preferences.userId],
|
||||||
references: [users.id],
|
references: [users.id],
|
||||||
|
|||||||
88
server/plugins/migrations.ts
Normal file
88
server/plugins/migrations.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { users, tokens, preferences } from "../database/schema";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
export default defineNitroPlugin(async () => {
|
||||||
|
const db = useDrizzle();
|
||||||
|
|
||||||
|
migrate(db, { migrationsFolder: "./server/database/migrations" });
|
||||||
|
console.log("Database migrations applied");
|
||||||
|
|
||||||
|
const feedPath = "./tmp/feed.json";
|
||||||
|
|
||||||
|
if (existsSync(feedPath)) {
|
||||||
|
const feed = JSON.parse(readFileSync(feedPath, "utf-8"));
|
||||||
|
console.log(`Importing ${feed.length} users from feed.json`);
|
||||||
|
|
||||||
|
for (const entry of feed) {
|
||||||
|
const userId = String(entry.user.id);
|
||||||
|
const createdAt =
|
||||||
|
entry.user.created_at && entry.user.created_at !== "\r"
|
||||||
|
? new Date(entry.user.created_at)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: userId,
|
||||||
|
name: entry.user.name || "Unknown",
|
||||||
|
avatar: entry.user.avatar || null,
|
||||||
|
city: entry.user.city || null,
|
||||||
|
country:
|
||||||
|
typeof entry.user.country === "string" && entry.user.country !== "\r"
|
||||||
|
? entry.user.country
|
||||||
|
: null,
|
||||||
|
sex:
|
||||||
|
typeof entry.user.sex === "string" && entry.user.sex !== "\r"
|
||||||
|
? entry.user.sex
|
||||||
|
: null,
|
||||||
|
weight: Number(entry.user.weight) || null,
|
||||||
|
createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(users)
|
||||||
|
.values(userData)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: users.id,
|
||||||
|
set: { ...userData },
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenData = {
|
||||||
|
userId: userId,
|
||||||
|
refreshToken: entry.token.refresh_token || null,
|
||||||
|
accessToken: entry.token.access_token || null,
|
||||||
|
expiresAt: new Date(entry.token.expires_at),
|
||||||
|
};
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(tokens)
|
||||||
|
.values(tokenData)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: tokens.userId,
|
||||||
|
set: { ...tokenData },
|
||||||
|
});
|
||||||
|
|
||||||
|
const prefsData = {
|
||||||
|
userId,
|
||||||
|
data: {
|
||||||
|
enabled: entry.preferences.enabled ?? true,
|
||||||
|
language: entry.preferences.language || "English",
|
||||||
|
units: entry.preferences.units || "Metric",
|
||||||
|
tone: entry.preferences.tone || [],
|
||||||
|
highlights: entry.preferences.highlights || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(preferences)
|
||||||
|
.values(prefsData)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: preferences.userId,
|
||||||
|
set: { ...prefsData },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully imported ${feed.length} users`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { PostHog } from "posthog-node";
|
|
||||||
import { waitUntil } from "@vercel/functions";
|
|
||||||
export default defineNitroPlugin((nitroApp) => {
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
|
|
||||||
const posthog = new PostHog(runtimeConfig.public.posthogPublicKey, {
|
|
||||||
host: runtimeConfig.public.posthogHost,
|
|
||||||
flushAt: 1,
|
|
||||||
flushInterval: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
nitroApp.hooks.hook("request", (event) => {
|
|
||||||
event.context.posthog = posthog;
|
|
||||||
});
|
|
||||||
|
|
||||||
nitroApp.hooks.hook("beforeResponse", () => {
|
|
||||||
waitUntil(posthog.shutdown());
|
|
||||||
});
|
|
||||||
|
|
||||||
nitroApp.hooks.hook("close", () => {
|
|
||||||
waitUntil(posthog.shutdown());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
declare module "h3" {
|
|
||||||
interface H3EventContext {
|
|
||||||
posthog: PostHog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,15 +24,13 @@ export default defineOAuthStravaEventHandler({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const posthog = event.context.posthog;
|
|
||||||
|
|
||||||
const userPayload = {
|
const userPayload = {
|
||||||
id: auth.user.id,
|
id: auth.user.id,
|
||||||
name: `${auth.user.firstname} ${auth.user.lastname}`,
|
name: `${auth.user.firstname} ${auth.user.lastname}`,
|
||||||
city: auth.user.city,
|
city: auth.user.city,
|
||||||
country: auth.user.country,
|
country: auth.user.country,
|
||||||
sex: auth.user.sex,
|
sex: auth.user.sex,
|
||||||
weight: auth.user.weight,
|
// weight: auth.user.weight,
|
||||||
avatar: auth.user.profile,
|
avatar: auth.user.profile,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,19 +80,6 @@ export default defineOAuthStravaEventHandler({
|
|||||||
user: userPayload,
|
user: userPayload,
|
||||||
});
|
});
|
||||||
|
|
||||||
posthog.identifyImmediate({
|
|
||||||
distinctId: String(user!.id),
|
|
||||||
properties: {
|
|
||||||
name: user!.name,
|
|
||||||
country: user!.country,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog.captureImmediate({
|
|
||||||
distinctId: String(user!.id),
|
|
||||||
event: "user logged in",
|
|
||||||
});
|
|
||||||
|
|
||||||
sendRedirect(event, "/");
|
sendRedirect(event, "/");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,49 +99,18 @@ export const createActivityContent = async ({
|
|||||||
const highlight = isEmpty(user.preferences.data?.highlights)
|
const highlight = isEmpty(user.preferences.data?.highlights)
|
||||||
? (draw(availableHighlights) as string)
|
? (draw(availableHighlights) as string)
|
||||||
: draw(user.preferences.data!.highlights!);
|
: draw(user.preferences.data!.highlights!);
|
||||||
const highlightInstructions = match({ highlight, activity: currentActivity })
|
|
||||||
.when(
|
|
||||||
({ highlight, activity }) =>
|
|
||||||
highlight === "Area Exploration" &&
|
|
||||||
movingActivityTypes.includes(get(activity, "type")),
|
|
||||||
() =>
|
|
||||||
"Focus on places visited and areas explored. Highlight any previous or new visits.",
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
{ highlight: "Athletic" },
|
|
||||||
() =>
|
|
||||||
"Highlight athletic properties and performance. Highlight PR's as well but only if available.",
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
{ highlight: "Mood" },
|
|
||||||
() =>
|
|
||||||
"Focus on how mood was swinging through the activity, ie I was feeling exhausted because of climb, I was feeling super happy on that descent!",
|
|
||||||
)
|
|
||||||
.with({ highlight: "Conditions" }, () => "Highlight on weather conditions")
|
|
||||||
.otherwise(() => "");
|
|
||||||
|
|
||||||
const length = match({ tone })
|
const length = match({ tone })
|
||||||
.with({ tone: "Minimalist" }, () => "short")
|
.with({ tone: "Minimalist" }, () => "very short")
|
||||||
.otherwise(() => draw(["short", "medium", "a-little-more-than-medium"]));
|
.otherwise(() => draw(["very short", "short"]));
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
Generate a short title and a ${length}-lengthed description for my strava activity. Use my preferred language and unit system.
|
|
||||||
Use first person, as this will be posting for myself. Try to not exaggerate as I am using Strava often and I want my activites to be unique and easy to read. Don't use repeative language.
|
|
||||||
Use a little bit of ${tone} tone to make things less boring.
|
|
||||||
${highlightInstructions}
|
|
||||||
Maybe comment if any interesting fact in comparison to previous activities.
|
|
||||||
|
|
||||||
Add #${tone} and #${highlight} at the end of the description. Depending the length of the description, maybe add more hashtags.
|
|
||||||
|
|
||||||
Language: ${user?.preferences.data!.language}
|
Language: ${user?.preferences.data!.language}
|
||||||
Unit system: ${user?.preferences.data!.units}
|
Unit system: ${user?.preferences.data!.units}
|
||||||
|
|
||||||
Activity notes:
|
Tone: ${tone}
|
||||||
Distance is in meters, time is in seconds, don't include average speed.
|
Highlight: ${highlight}
|
||||||
Convert time to hours or minutes, whatever's closer.
|
Description length: ${length}
|
||||||
Convert distance to larger units when appropriate, we don't need accuracy. Better say almost 50 instead of 48.67 for example.
|
|
||||||
|
|
||||||
In the end of the description, add "${promo}" translated to my language.
|
|
||||||
|
|
||||||
The activity data in json format from strava:
|
The activity data in json format from strava:
|
||||||
${stringifyActivity({ activity: currentActivity })}
|
${stringifyActivity({ activity: currentActivity })}
|
||||||
@@ -151,8 +120,11 @@ export const createActivityContent = async ({
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const aiResponse = await openai.responses.create({
|
const aiResponse = await openai.responses.create({
|
||||||
model: "gpt-5-mini",
|
model: "@preset/ghostwriter",
|
||||||
input: [{ role: "user", content: prompt }],
|
input: [{ role: "user", content: prompt }],
|
||||||
|
reasoning: {
|
||||||
|
effort: "minimal",
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
format: {
|
format: {
|
||||||
type: "json_schema",
|
type: "json_schema",
|
||||||
@@ -184,10 +156,6 @@ export const createActivityContent = async ({
|
|||||||
const stravaRequestBody = {
|
const stravaRequestBody = {
|
||||||
name: responseObject!.title,
|
name: responseObject!.title,
|
||||||
description: responseObject!.description,
|
description: responseObject!.description,
|
||||||
meta: {
|
|
||||||
highlight,
|
|
||||||
tone,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return [parseError, stravaRequestBody] as const;
|
return [parseError, stravaRequestBody] as const;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { drizzle } from "drizzle-orm/neon-http";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
export { sql, eq, and, or } from "drizzle-orm";
|
|
||||||
|
|
||||||
import * as schema from "../database/schema";
|
import * as schema from "../database/schema";
|
||||||
|
|
||||||
export const tables = schema;
|
export const tables = schema;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export const useOpenAI = () => {
|
|||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: config.openaiApiKey,
|
apiKey: config.openrouterApiKey,
|
||||||
|
baseURL: "https://openrouter.ai/api/v1",
|
||||||
});
|
});
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { PostHog } from "posthog-node";
|
|
||||||
|
|
||||||
let client: PostHog;
|
|
||||||
|
|
||||||
export const usePosthog = () => {
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
|
|
||||||
client =
|
|
||||||
client ??
|
|
||||||
new PostHog(runtimeConfig.public.posthogPublicKey, {
|
|
||||||
host: runtimeConfig.public.posthogHost,
|
|
||||||
defaults: runtimeConfig.public.posthogDefaults,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.dev) {
|
|
||||||
client.debug();
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user