Compare commits
10 Commits
6215e21bed
...
41c71a9877
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c71a9877 | |||
| d8be1c0a69 | |||
| 1b31f3194d | |||
| 27b7d87e68 | |||
| 01e346e157 | |||
| 1df8a3f452 | |||
| aad888ff2c | |||
| 906c9765b5 | |||
| 559a7d9a21 | |||
| 284c792a92 |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.gitignore
|
||||
.nuxt
|
||||
.output
|
||||
node_modules
|
||||
*.md
|
||||
README*
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
17
.env.example
17
.env.example
@@ -1,9 +1,8 @@
|
||||
NUXT_OAUTH_STRAVA_CLIENT_ID=[YOUR_STRAVA_CLIENT_ID]
|
||||
NUXT_OAUTH_STRAVA_CLIENT_SECRET=[YOUR_STRAVA_CLIENT_SECRET]
|
||||
NUXT_SESSION_PASSWORD=[YOUR_SESSION_PASSWORD]
|
||||
NUXT_STRAVA_VERIFY_TOKEN=[YOUR_STRAVA_VERIFY_TOKEN]
|
||||
NUXT_OPENAI_API_KEY=[YOUR_OPENAI_API_KEY]
|
||||
NUXT_PUBLIC_APTABASE_APP_KEY=[YOUR_APTABASE_APP_KEY]
|
||||
NUXT_WEBHOOKS_URL=[YOUR_WEBHOOKS_URL]
|
||||
NUXT_DATABASE_URL=[YOUR_DATABASE_URL]
|
||||
NUXT_HOOKDECK_KEY=[YOUR_HOOKDECK_KEY]
|
||||
NUXT_OAUTH_STRAVA_CLIENT_ID=your_client_id
|
||||
NUXT_OAUTH_STRAVA_CLIENT_SECRET=your_client_secret
|
||||
NUXT_SESSION_PASSWORD=your_session_password_min_32_chars
|
||||
NUXT_STRAVA_VERIFY_TOKEN=your_verify_token
|
||||
NUXT_OPENAI_API_KEY=your_openai_api_key
|
||||
NUXT_DATABASE_URL=file:./tmp/ghostwriter.db
|
||||
NUXT_HOOKDECK_KEY=your_hookdeck_key
|
||||
NUXT_OPENROUTER_API_KEY=your_openrouter_api_key
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ logs
|
||||
.env.*
|
||||
!.env.example
|
||||
.vercel
|
||||
|
||||
tmp/
|
||||
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM oven/bun:1-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nuxt
|
||||
|
||||
COPY --from=builder /app/.output ./.output
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
USER nuxt
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["bun", "x", "nuxt", "start"]
|
||||
53
README.md
53
README.md
@@ -2,24 +2,47 @@
|
||||
|
||||
## 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
|
||||
git clone [repository_url]
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Navigate to the project directory:
|
||||
|
||||
2. Install and run:
|
||||
```bash
|
||||
cd ghostwriter
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
4. Run the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
5. Access the application at http://localhost:3000
|
||||
|
||||
Open http://localhost:3000 and start ghostwriting those activities into legend.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `bun run dev` | Spin up the dev server |
|
||||
| `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
|
||||
bun run dev
|
||||
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Generate static site
|
||||
bun run generate
|
||||
|
||||
# Type checking
|
||||
bunx vue-tsc --noEmit
|
||||
|
||||
# Database
|
||||
bunx drizzle-kit push
|
||||
bunx drizzle-kit studio
|
||||
```
|
||||
|
||||
## Project Info
|
||||
|
||||
- Nuxt 4
|
||||
- Uses bun as package manager and runtime
|
||||
- Database: Drizzle ORM with Bun's sqlite
|
||||
- UI: @nuxt/ui v3
|
||||
@@ -13,7 +13,9 @@ const { openInPopup } = useUserSession();
|
||||
<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 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
|
||||
</div>
|
||||
<NuxtImg src="/ghostwriter-logo.png" class="size-9" />
|
||||
@@ -23,8 +25,8 @@ const { openInPopup } = useUserSession();
|
||||
Sign in to your account.
|
||||
</div>
|
||||
<div class="text-center">
|
||||
Connect with Strava to automatically add personalized titles and
|
||||
descriptions to your activities.
|
||||
Connect with Strava to automatically add personalized
|
||||
titles and descriptions to your activities.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from "drizzle-kit";
|
||||
import { get } from "radash";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
dialect: "sqlite",
|
||||
schema: "./server/database/schema.ts",
|
||||
out: "./server/database/migrations",
|
||||
dbCredentials: {
|
||||
|
||||
@@ -12,13 +12,8 @@ export default defineNuxtConfig({
|
||||
webhooksUrl: "",
|
||||
stravaVerifyToken: "",
|
||||
hookdeckKey: "",
|
||||
openaiApiKey: "",
|
||||
openrouterApiKey: "",
|
||||
databaseUrl: "",
|
||||
public: {
|
||||
posthogPublicKey: "",
|
||||
posthogHost: "",
|
||||
posthogDefaults: "2025-05-24",
|
||||
},
|
||||
},
|
||||
future: { compatibilityVersion: 4 },
|
||||
compatibilityDate: "2025-03-01",
|
||||
|
||||
16152
package-lock.json
generated
16152
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,21 +10,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/tempo": "^0.1.2",
|
||||
"@google/genai": "^1.13.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@nuxt/icon": "1.15.0",
|
||||
"@nuxt/image": "^1.11.0",
|
||||
"@nuxt/ui": "3.3.0",
|
||||
"@vee-validate/nuxt": "^4.15.1",
|
||||
"@vercel/functions": "^2.2.8",
|
||||
"@vueuse/nuxt": "^13.6.0",
|
||||
"destr": "^2.0.5",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"nuxt": "^4.0.3",
|
||||
"nuxt-auth-utils": "0.5.23",
|
||||
"openai": "^5.12.2",
|
||||
"posthog-js": "^1.259.0",
|
||||
"posthog-node": "^5.6.0",
|
||||
"radash": "^12.1.1",
|
||||
"ts-pattern": "^5.8.0",
|
||||
"url": "^0.11.4",
|
||||
@@ -33,6 +28,9 @@
|
||||
"zod": "^4.0.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/heroicons": "^1.2.3",
|
||||
"@iconify-json/lucide": "^1.2.95",
|
||||
"@types/bun": "^1.3.10",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"typescript": "^5.9.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
|
||||
@@ -54,14 +54,14 @@ const saveOp = watchPausable(
|
||||
<div class="font-bold text-lg">Welcome to Ghostwriter!</div>
|
||||
|
||||
<div>
|
||||
Let's generate fun and engaging titles and descriptions for your Strava
|
||||
activities automatically, right when they are created. Customize your
|
||||
preferences below.
|
||||
Let's generate fun and engaging titles and descriptions for your
|
||||
Strava activities automatically, right when they are created.
|
||||
Customize your preferences below.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Add a touch of creativity to your Strava workouts. Simply enable it and
|
||||
choose your language, and we'll do the rest!
|
||||
Add a touch of creativity to your Strava workouts. Simply enable it
|
||||
and choose your language, and we'll do the rest!
|
||||
</div>
|
||||
</UContainer>
|
||||
|
||||
@@ -69,12 +69,15 @@ const saveOp = watchPausable(
|
||||
<div class="font-bold text-lg">❤️ Support</div>
|
||||
<UCard class="">
|
||||
<div class="flex flex-col gap-8">
|
||||
Ghostwriter 👻 is free to use, but it takes time and resources to keep
|
||||
it running smoothly. If you enjoy it, consider supporting the app and
|
||||
its creator - every bit helps!
|
||||
Ghostwriter 👻 is free to use, but it takes time and resources
|
||||
to keep it running smoothly. If you enjoy it, consider
|
||||
supporting the app and its creator - every bit helps!
|
||||
</div>
|
||||
<template #footer>
|
||||
<ULink href="https://buymeacoffee.com/mariosant" target="_blank">
|
||||
<ULink
|
||||
href="https://buymeacoffee.com/mariosant"
|
||||
target="_blank"
|
||||
>
|
||||
<NuxtImg
|
||||
src="images/bmac-orange-button.png"
|
||||
height="32px"
|
||||
@@ -190,11 +193,15 @@ const saveOp = watchPausable(
|
||||
<CardField>
|
||||
<template #title> Athlete ID </template>
|
||||
<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>
|
||||
<ULink :href="stravaLink" class="underline flex items-center gap-2">
|
||||
<ULink
|
||||
:href="stravaLink"
|
||||
class="underline flex items-center gap-2"
|
||||
>
|
||||
{{ user.id }}
|
||||
<UIcon
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
10627
pnpm-lock.yaml
generated
10627
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,
|
||||
availableUnits,
|
||||
} from "~/shared/constants";
|
||||
//
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
enabled: z.boolean(),
|
||||
language: z.enum(availableLanguages),
|
||||
units: z.enum(availableUnits),
|
||||
tone: z.array(z.enum(availableTones)),
|
||||
highlights: z.array(z.enum(availableHighlights)),
|
||||
units: z.enum(availableUnits).default(availableUnits[1]),
|
||||
tone: z.array(z.enum(availableTones)).default([]),
|
||||
highlights: z.array(z.enum(availableHighlights)).default([]),
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { get } from "radash";
|
||||
import { createActivityContent } from "~~/server/utils/create-content";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await validateHookdeck(event);
|
||||
|
||||
const posthog = event.context.posthog;
|
||||
|
||||
const body = await readBody(event);
|
||||
const db = useDrizzle();
|
||||
|
||||
@@ -25,7 +22,7 @@ export default defineEventHandler(async (event) => {
|
||||
const currentActivity = await strava!<any>(`/activities/${body.object_id}`);
|
||||
const [, ...previousActivities] = await strava!<any[]>(`/activities`, {
|
||||
query: {
|
||||
per_page: 20,
|
||||
per_page: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,8 +41,11 @@ export default defineEventHandler(async (event) => {
|
||||
await strava!(`activities/${body.object_id}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
name: stravaRequestBody.name,
|
||||
description: stravaRequestBody.description,
|
||||
name: (stravaRequestBody.name as String).replaceAll("—", ","),
|
||||
description: (stravaRequestBody.description as String).replaceAll(
|
||||
"—",
|
||||
",",
|
||||
),
|
||||
},
|
||||
}).catch((error) => {
|
||||
throw createError({
|
||||
@@ -53,15 +53,4 @@ export default defineEventHandler(async (event) => {
|
||||
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) => {
|
||||
await validateHookdeck(event);
|
||||
|
||||
const posthog = event.context.posthog;
|
||||
|
||||
const body = await readBody(event);
|
||||
const db = useDrizzle();
|
||||
|
||||
@@ -13,29 +11,9 @@ export default defineEventHandler(async (event) => {
|
||||
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
|
||||
.delete(tables.users)
|
||||
.where(eq(tables.users.id, get(body, "object_id")));
|
||||
|
||||
posthog.captureImmediate({
|
||||
distinctId: get(body, "object_id"),
|
||||
event: "user deleted",
|
||||
});
|
||||
|
||||
sendNoContent(event);
|
||||
});
|
||||
|
||||
28
server/database/migrations/0000_living_oracle.sql
Normal file
28
server/database/migrations/0000_living_oracle.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 NOT NULL
|
||||
);
|
||||
@@ -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;
|
||||
16
server/database/migrations/0001_round_husk.sql
Normal file
16
server/database/migrations/0001_round_husk.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_users` (
|
||||
`id` numeric PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`avatar` text,
|
||||
`city` text,
|
||||
`country` text,
|
||||
`sex` text,
|
||||
`weight` integer,
|
||||
`created_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_users`("id", "name", "avatar", "city", "country", "sex", "weight", "created_at") SELECT "id", "name", "avatar", "city", "country", "sex", "weight", "created_at" FROM `users`;--> statement-breakpoint
|
||||
DROP TABLE `users`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -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": "d9537942-cba2-4af4-8396-533366512937",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.preferences": {
|
||||
"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
|
||||
}
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"type": "text",
|
||||
"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": {
|
||||
"preferences_user_id_users_id_fk": {
|
||||
"name": "preferences_user_id_users_id_fk",
|
||||
"tableFrom": "preferences",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"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"]
|
||||
}
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tokens": {
|
||||
"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
|
||||
}
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tokens_user_id_unique": {
|
||||
"name": "tokens_user_id_unique",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"tokens_user_id_users_id_fk": {
|
||||
"name": "tokens_user_id_users_id_fk",
|
||||
"tableFrom": "tokens",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"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"]
|
||||
}
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"avatar": {
|
||||
"name": "avatar",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sex": {
|
||||
"name": "sex",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"weight": {
|
||||
"name": "weight",
|
||||
"type": "numeric",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +1,204 @@
|
||||
{
|
||||
"id": "912c6e36-57b1-4e7f-a29b-586b187b1c32",
|
||||
"prevId": "c8519a52-b999-48f3-b532-42a5678e3905",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "80d98e5a-f977-4e9c-889b-ae77c99d8238",
|
||||
"prevId": "d9537942-cba2-4af4-8396-533366512937",
|
||||
"tables": {
|
||||
"public.preferences": {
|
||||
"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
|
||||
}
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "jsonb",
|
||||
"type": "text",
|
||||
"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": {
|
||||
"preferences_user_id_users_id_fk": {
|
||||
"name": "preferences_user_id_users_id_fk",
|
||||
"tableFrom": "preferences",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"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"]
|
||||
}
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tokens": {
|
||||
"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
|
||||
}
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tokens_user_id_unique": {
|
||||
"name": "tokens_user_id_unique",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"tokens_user_id_users_id_fk": {
|
||||
"name": "tokens_user_id_users_id_fk",
|
||||
"tableFrom": "tokens",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"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"]
|
||||
}
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"type": "numeric",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"avatar": {
|
||||
"name": "avatar",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sex": {
|
||||
"name": "sex",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"premium": {
|
||||
"name": "premium",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"weight": {
|
||||
"name": "weight",
|
||||
"type": "numeric",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -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,19 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1745335305323,
|
||||
"tag": "0000_slim_blonde_phantom",
|
||||
"version": "6",
|
||||
"when": 1772727834477,
|
||||
"tag": "0000_living_oracle",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1747547391071,
|
||||
"tag": "0001_smooth_jazinda",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1747564780270,
|
||||
"tag": "0002_many_marauders",
|
||||
"version": "6",
|
||||
"when": 1772816922744,
|
||||
"tag": "0001_round_husk",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
integer,
|
||||
numeric,
|
||||
timestamp,
|
||||
jsonb,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sqliteTable, text, integer, numeric } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: integer("id").primaryKey(),
|
||||
export const users = sqliteTable("users", {
|
||||
id: numeric("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
avatar: text("avatar").notNull(),
|
||||
avatar: text("avatar"),
|
||||
city: text("city"),
|
||||
country: text("country"),
|
||||
sex: text("sex"),
|
||||
weight: numeric("weight", {
|
||||
mode: "number",
|
||||
}),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
weight: integer("weight"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
||||
() => new Date(),
|
||||
),
|
||||
});
|
||||
|
||||
export const preferences = pgTable("preferences", {
|
||||
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
||||
userId: integer("user_id")
|
||||
export const preferences = sqliteTable("preferences", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: numeric("user_id")
|
||||
.references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
})
|
||||
.unique(),
|
||||
data: jsonb("data")
|
||||
data: text("data", { mode: "json" })
|
||||
.$type<{
|
||||
enabled: boolean;
|
||||
language: string;
|
||||
@@ -45,19 +38,20 @@ export const preferences = pgTable("preferences", {
|
||||
})),
|
||||
});
|
||||
|
||||
export const tokens = pgTable("tokens", {
|
||||
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
||||
userId: integer("user_id")
|
||||
export const tokens = sqliteTable("tokens", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: numeric("user_id")
|
||||
.references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
})
|
||||
.unique(),
|
||||
refreshToken: text("refresh_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 }) => ({
|
||||
tokens: one(tokens, {
|
||||
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, {
|
||||
fields: [preferences.userId],
|
||||
references: [users.id],
|
||||
|
||||
116
server/plugins/feed.ts
Normal file
116
server/plugins/feed.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import * as schema from "../database/schema";
|
||||
|
||||
interface FeedItem {
|
||||
user: {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string | null;
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
sex: string | null;
|
||||
weight: number | null;
|
||||
created_at: string;
|
||||
};
|
||||
token: {
|
||||
id: number;
|
||||
user_id: number;
|
||||
refresh_token: string;
|
||||
access_token: string;
|
||||
expires_at: string;
|
||||
};
|
||||
preferences: {
|
||||
tone: string[];
|
||||
units: "Imperial" | "Metric";
|
||||
enabled: boolean;
|
||||
language: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
const config = useRuntimeConfig();
|
||||
const feedPath = "tmp/feed.json";
|
||||
|
||||
if (!existsSync(feedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Processing feed.json...");
|
||||
|
||||
const db = drizzle(config.databaseUrl, { schema });
|
||||
const feed: FeedItem[] = JSON.parse(readFileSync(feedPath, "utf-8"));
|
||||
|
||||
for (const item of feed) {
|
||||
const userId = String(item.user.id);
|
||||
|
||||
await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
id: userId,
|
||||
name: item.user.name,
|
||||
avatar: item.user.avatar,
|
||||
city: item.user.city,
|
||||
country: item.user.country,
|
||||
sex: item.user.sex,
|
||||
weight: item.user.weight,
|
||||
createdAt: new Date(item.user.created_at),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.users.id,
|
||||
set: {
|
||||
name: item.user.name,
|
||||
avatar: item.user.avatar,
|
||||
city: item.user.city,
|
||||
country: item.user.country,
|
||||
sex: item.user.sex,
|
||||
weight: item.user.weight,
|
||||
},
|
||||
});
|
||||
|
||||
await db
|
||||
.insert(schema.tokens)
|
||||
.values({
|
||||
userId: userId,
|
||||
refreshToken: item.token.refresh_token,
|
||||
accessToken: item.token.access_token,
|
||||
expiresAt: new Date(item.token.expires_at),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.tokens.userId,
|
||||
set: {
|
||||
refreshToken: item.token.refresh_token,
|
||||
accessToken: item.token.access_token,
|
||||
expiresAt: new Date(item.token.expires_at),
|
||||
},
|
||||
});
|
||||
|
||||
await db
|
||||
.insert(schema.preferences)
|
||||
.values({
|
||||
userId: userId,
|
||||
data: {
|
||||
enabled: item.preferences.enabled,
|
||||
language: item.preferences.language,
|
||||
units: item.preferences.units,
|
||||
tone: item.preferences.tone,
|
||||
highlights: [],
|
||||
},
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.preferences.userId,
|
||||
set: {
|
||||
data: {
|
||||
enabled: item.preferences.enabled,
|
||||
language: item.preferences.language,
|
||||
units: item.preferences.units,
|
||||
tone: item.preferences.tone,
|
||||
highlights: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Processed ${feed.length} feed items`);
|
||||
});
|
||||
12
server/plugins/migrations.ts
Normal file
12
server/plugins/migrations.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import * as schema from "../database/schema";
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
const config = useRuntimeConfig();
|
||||
const db = drizzle(config.databaseUrl, { schema });
|
||||
|
||||
console.log("Running database migrations...");
|
||||
await migrate(db, { migrationsFolder: "./server/database/migrations" });
|
||||
console.log("Database migrations complete");
|
||||
});
|
||||
@@ -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 = {
|
||||
id: auth.user.id,
|
||||
name: `${auth.user.firstname} ${auth.user.lastname}`,
|
||||
city: auth.user.city,
|
||||
country: auth.user.country,
|
||||
sex: auth.user.sex,
|
||||
weight: auth.user.weight,
|
||||
// weight: auth.user.weight,
|
||||
avatar: auth.user.profile,
|
||||
};
|
||||
|
||||
@@ -82,19 +80,6 @@ export default defineOAuthStravaEventHandler({
|
||||
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, "/");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,49 +99,18 @@ export const createActivityContent = async ({
|
||||
const highlight = isEmpty(user.preferences.data?.highlights)
|
||||
? (draw(availableHighlights) as string)
|
||||
: 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 })
|
||||
.with({ tone: "Minimalist" }, () => "short")
|
||||
.otherwise(() => draw(["short", "medium", "a-little-more-than-medium"]));
|
||||
.with({ tone: "Minimalist" }, () => "very short")
|
||||
.otherwise(() => draw(["very short", "short"]));
|
||||
|
||||
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}
|
||||
Unit system: ${user?.preferences.data!.units}
|
||||
|
||||
Activity notes:
|
||||
Distance is in meters, time is in seconds, don't include average speed.
|
||||
Convert time to hours or minutes, whatever's closer.
|
||||
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.
|
||||
Tone: ${tone}
|
||||
Highlight: ${highlight}
|
||||
Description length: ${length}
|
||||
|
||||
The activity data in json format from strava:
|
||||
${stringifyActivity({ activity: currentActivity })}
|
||||
@@ -151,8 +120,11 @@ export const createActivityContent = async ({
|
||||
`;
|
||||
|
||||
const aiResponse = await openai.responses.create({
|
||||
model: "gpt-5-mini",
|
||||
model: "@preset/ghostwriter",
|
||||
input: [{ role: "user", content: prompt }],
|
||||
reasoning: {
|
||||
effort: "minimal",
|
||||
},
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
@@ -184,10 +156,6 @@ export const createActivityContent = async ({
|
||||
const stravaRequestBody = {
|
||||
name: responseObject!.title,
|
||||
description: responseObject!.description,
|
||||
meta: {
|
||||
highlight,
|
||||
tone,
|
||||
},
|
||||
};
|
||||
|
||||
return [parseError, stravaRequestBody] as const;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { drizzle } from "drizzle-orm/neon-http";
|
||||
export { sql, eq, and, or } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { eq as _eq, and as _and, or as _or, sql as _sql } from "drizzle-orm";
|
||||
|
||||
export const sql = _sql;
|
||||
export const eq = _eq;
|
||||
export const and = _and;
|
||||
export const or = _or;
|
||||
|
||||
import * as schema from "../database/schema";
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ export const useOpenAI = () => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: config.openaiApiKey,
|
||||
apiKey: config.openrouterApiKey,
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
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