Compare commits

..

10 Commits

Author SHA1 Message Date
1117b4adc0 add drizzle schema 2026-03-07 13:24:43 +02:00
8de1f87435 remove pnpm workspace 2026-03-07 12:49:48 +02:00
d82416e4a3 remove build config 2026-03-07 12:36:27 +02:00
926fc0fd0b Move to better sqlite3 2026-03-07 12:34:04 +02:00
0370930894 Update preset 2026-03-07 09:56:33 +02:00
8831857007 directly run server 2026-03-06 22:06:57 +02:00
095463cafa Update migrations 2026-03-06 21:59:52 +02:00
742242ffb8 Add nixpacks.toml for Railway deployment with bun 2026-03-06 19:44:10 +02:00
61de53c014 Update lockfile 2026-03-06 19:33:45 +02:00
cda02e35fb Add start task 2026-03-06 19:28:55 +02:00
15 changed files with 10644 additions and 3277 deletions

View File

@@ -1,10 +0,0 @@
.git
.gitignore
.nuxt
.output
node_modules
*.md
README*
.env
.env.*
!.env.example

View File

@@ -1,28 +0,0 @@
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"]

View File

@@ -4,25 +4,25 @@
```bash ```bash
# Development # Development
bun run dev pnpm run dev
# Build # Build
bun run build pnpm run build
# Generate static site # Generate static site
bun run generate pnpm run generate
# Type checking # Type checking
bunx vue-tsc --noEmit pnpm exec vue-tsc --noEmit
# Database # Database
bunx drizzle-kit push pnpm exec drizzle-kit push
bunx drizzle-kit studio pnpm exec drizzle-kit studio
``` ```
## Project Info ## Project Info
- Nuxt 4 - Nuxt 4
- Uses bun as package manager and runtime - Uses pnpm as package manager and runtime
- Database: Drizzle ORM with Bun's sqlite - Database: Drizzle ORM with sqlite
- UI: @nuxt/ui v3 - UI: @nuxt/ui v4

2850
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -50,4 +50,10 @@ export default defineNuxtConfig({
], ],
}, },
}, },
nitro: {
preset: "node-server",
experimental: {
tasks: true,
},
},
}); });

View File

@@ -6,40 +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",
"@nuxt/icon": "1.15.0", "@nuxt/icon": "^2.2.1",
"@nuxt/image": "^1.11.0", "@nuxt/image": "^2.0.0",
"@nuxt/ui": "3.3.0", "@nuxt/ui": "4.5.1",
"@vee-validate/nuxt": "^4.15.1", "@vee-validate/nuxt": "^4.15.1",
"@vueuse/nuxt": "^13.6.0", "@vueuse/nuxt": "^14.2.1",
"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",
"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/heroicons": "^1.2.3",
"@iconify-json/lucide": "^1.2.95", "@iconify-json/lucide": "^1.2.95",
"@types/bun": "^1.3.10", "@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"
] ]
}
} }

10514
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,5 +24,5 @@ CREATE TABLE `users` (
`country` text, `country` text,
`sex` text, `sex` text,
`weight` integer, `weight` integer,
`created_at` integer NOT NULL `created_at` integer
); );

View File

@@ -1,16 +0,0 @@
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;

View File

@@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "d9537942-cba2-4af4-8396-533366512937", "id": "347ea848-a216-4fff-910a-90ba2f9aa91c",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"preferences": { "preferences": {
@@ -180,7 +180,7 @@
"name": "created_at", "name": "created_at",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false "autoincrement": false
} }
}, },

View File

@@ -1,204 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "80d98e5a-f977-4e9c-889b-ae77c99d8238",
"prevId": "d9537942-cba2-4af4-8396-533366512937",
"tables": {
"preferences": {
"name": "preferences",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "numeric",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"data": {
"name": "data",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"preferences_user_id_unique": {
"name": "preferences_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"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": {},
"checkConstraints": {}
},
"tokens": {
"name": "tokens",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "numeric",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tokens_user_id_unique": {
"name": "tokens_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"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": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "numeric",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -5,15 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1772727834477, "when": 1772879556762,
"tag": "0000_living_oracle", "tag": "0000_groovy_rachel_grey",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1772816922744,
"tag": "0001_round_husk",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,116 +0,0 @@
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`);
});

View File

@@ -1,12 +1,88 @@
import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { drizzle } from "drizzle-orm/better-sqlite3";
import { drizzle } from "drizzle-orm/bun-sqlite"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import * as schema from "../database/schema"; import Database from "better-sqlite3";
import { users, tokens, preferences } from "../database/schema";
import { existsSync, readFileSync } from "node:fs";
export default defineNitroPlugin(async () => { export default defineNitroPlugin(async () => {
const config = useRuntimeConfig(); const db = useDrizzle();
const db = drizzle(config.databaseUrl, { schema });
console.log("Running database migrations..."); migrate(db, { migrationsFolder: "./server/database/migrations" });
await migrate(db, { migrationsFolder: "./server/database/migrations" }); console.log("Database migrations applied");
console.log("Database migrations complete");
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`);
}
}); });

View File

@@ -1,11 +1,4 @@
import { drizzle } from "drizzle-orm/bun-sqlite"; import { drizzle } from "drizzle-orm/better-sqlite3";
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"; import * as schema from "../database/schema";
export const tables = schema; export const tables = schema;