Compare commits

..

No commits in common. "29086b57fd9ee3dd1034b6b23210c678dca4a435" and "aaadc4f4d6e2e5016e28815592d733ff59f02b9d" have entirely different histories.

50 changed files with 720 additions and 5603 deletions

View File

@ -1,4 +0,0 @@
DATABASE_URL=postgres://user:password@127.0.0.1:5432/postgres
REDIS_URL=redis://127.0.0.1:6379/
PORT=8080
HOST=127.0.0.1

21
.eslintrc.json Normal file
View File

@ -0,0 +1,21 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": "xo",
"overrides": [
{
"extends": ["xo-typescript"],
"files": ["*.ts", "*.tsx"]
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
// double quotes
"@typescript-eslint/quotes": "double"
}
}

4
.gitignore vendored
View File

@ -1,5 +1,3 @@
node_modules node_modules
env env
yarn-error.log yarn-error.log
dist
.env

View File

@ -1,3 +0,0 @@
{
"recommendations": ["samverschueren.linter-xo"]
}

View File

@ -1,38 +1,3 @@
# fastify-jsdoc-quick-start # fastify-drizzle-quick-start
Quick start for Fastify, JSDoc, ESlint, Prettier and DrizzleORM Quick start for Fastify, TypeScript, ESlint, Prettier and DrizzleORM
## Quick Start
### Comes equipped with
- Dockerfile including postgres and redis
- JSDoc type annotations
- XO
- Prettier
- DrizzleORM
- Fastify
- Zod
### Prerequisites
- [Docker](https://docs.docker.com/get-docker/)
- [Yarn](https://yarnpkg.com/getting-started/install)
- [NodeJS](https://nodejs.org/en/download/)
### Getting Started
1. Clone the repo
2. Run `yarn install`
3. Run `docker-compose up -d` (-d to detach from the terminal)
4. Run `yarn dev` to start the dev server
5. Make your changes
6. Run `yarn lint` to lint your code
7. Run `yarn start` to start the production server
# Credits
Originally cloned from https://github.com/Looskie/fastify-drizzle-quick-start and transformed to use JSDoc over Typescript

View File

@ -1,12 +0,0 @@
import * as dotenv from 'dotenv'
dotenv.config()
export default {
"out": "./src/db/migrations",
"schema": "./src/db/schemas.js",
"driver": "pg",
"dbCredentials": {
"connectionString": process.env.DATABASE_URL
}
}

4
drizzle.config.json Normal file
View File

@ -0,0 +1,4 @@
{
"out": "./src/db/migrations",
"schema": "./src/db/schemas.ts"
}

View File

@ -2,66 +2,38 @@
"name": "fastify-drizzle-quick-start", "name": "fastify-drizzle-quick-start",
"version": "1.0.0", "version": "1.0.0",
"description": "Quick start for Fastify, TypeScript, ESlint, Prettier and DrizzleORM", "description": "Quick start for Fastify, TypeScript, ESlint, Prettier and DrizzleORM",
"type": "module",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/Looskie/fastify-drizzle-quick-start.git", "repository": "https://github.com/Looskie/fastify-drizzle-quick-start.git",
"author": "Cody Miller <50378828+Looskie@users.noreply.github.com>", "author": "Cody Miller <50378828+Looskie@users.noreply.github.com>",
"license": "none", "license": "none",
"scripts": { "scripts": {
"lint": "xo", "lint": "eslint . --ext .ts",
"dev": "tsx watch --clear-screen=false src/index.js", "dev": "tsx watch --clear-screen=false src/index.ts",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"migrate": "drizzle-kit generate:pg", "migrate": "drizzle-kit generate:pg"
"start": "node src/index.js"
}, },
"devDependencies": { "devDependencies": {
"@types/cli-color": "^2.0.6", "@types/cli-color": "^2.0.2",
"@types/node": "^20.10.4", "@types/node": "^20.2.5",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.2",
"@typescript-eslint/eslint-plugin": ">=5.57.0",
"@typescript-eslint/parser": ">=5.57.0",
"cli-color": "^2.0.3", "cli-color": "^2.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.1.4",
"drizzle-kit": "^0.20.6", "drizzle-kit": "^0.18.1",
"prettier": "^3.1.1", "eslint": ">=8.0.0",
"tsc-alias": "^1.8.8", "eslint-config-xo": "^0.43.1",
"eslint-config-xo-typescript": "^0.57.0",
"prettier": "^2.8.8",
"tsx": "^3.12.7", "tsx": "^3.12.7",
"typescript": "^5.3.3", "typescript": ">=4.4"
"xo": "^0.56.0"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "^8.4.2", "@fastify/cors": "^8.3.0",
"@fastify/oauth2": "^7.8.0", "drizzle-orm": "^0.26.5",
"drizzle-orm": "^0.29.1", "fastify": "^4.18.0",
"fastify": "^4.25.0", "pg": "^8.11.0",
"fastify-plugin": "^4.5.1", "redis": "^4.6.7",
"googleapis": "^134.0.0", "zod": "^3.21.4"
"pg": "^8.11.3",
"redis": "^4.6.11",
"simple-get": "^4.0.1",
"tsc": "^2.0.4",
"zod": "^3.22.4"
},
"xo": {
"rules": {
"import/extensions": "off",
"@typescript-eslint/quotes": [
"error",
"double"
],
"@typescript-eslint/indent": [
"error",
2
],
"@typescript-eslint/object-curly-spacing": [
"error",
"always"
],
"n/prefer-global/process": "off",
"@typescript-eslint/no-extraneous-class": "off",
"arrow-parens": [
"error",
"always"
],
"@typescript-eslint/no-floating-promises": "off"
}
} }
} }

View File

@ -1,41 +0,0 @@
import * as schema from "./schemas.js";
import { env, Logger } from "../utils/index.js";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import pgpkg from 'pg';
const { Pool } = pgpkg;
/** @type {ReturnType<typeof drizzle<typeof schema>>} */
export let db;
export const initDb = async () => {
const pool = await new Pool({
connectionString: env.DATABASE_URL,
idleTimeoutMillis: 20000
})
.connect()
.then((client) => {
Logger.info("INIT", "Connected to database");
return client;
})
.catch((error) => {
Logger.error("INIT", `Failed to connect to database ${String(error)}}`);
throw new Error(`Failed to connect to database ${String(error)}`);
});
db = drizzle(pool, {
schema,
});
// await migrate(db, {
// migrationsFolder: "./src/db/migrations",
// })
// .then(() => {
// Logger.info("INIT", "Migrated database");
// })
// .catch((error) => {
// Logger.error("INIT", `Failed to migrate database ${String(error)}`);
// throw new Error(`Failed to migrate database ${String(error)}`);
// });
};

36
src/db/index.ts Normal file
View File

@ -0,0 +1,36 @@
import * as schema from "@api/db/schemas";
import { env, Logger } from "@api/utils";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { Pool } from "pg";
export let db: ReturnType<typeof drizzle<typeof schema>>;
export const initDb = async () => {
const pool = await new Pool({
connectionString: env.DATABASE_URL,
})
.connect()
.then((client) => {
Logger.info("INIT", "Connected to database");
return client;
})
.catch((err) => {
Logger.error("INIT", `Failed to connect to database ${String(err)}}`);
process.exit(1);
});
db = drizzle(pool);
await migrate(db, {
migrationsFolder: "./src/db/migrations",
})
.then(() => {
Logger.info("INIT", "Migrated database");
})
.catch((err) => {
Logger.error("INIT", `Failed to migrate database ${String(err)}`);
process.exit(1);
});
};

View File

@ -1,43 +0,0 @@
CREATE TABLE IF NOT EXISTS "articles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"site_id" uuid,
"content" text,
"source_video_id" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid,
"google_code" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid,
"name" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"google_id" text,
"name" text,
"email" text
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "articles" ADD CONSTRAINT "articles_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sites" ADD CONSTRAINT "sites_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
"full_name" text,
"phone" varchar(256)
);

View File

@ -1,6 +0,0 @@
ALTER TABLE "sessions" ADD COLUMN "google_access_token" text;--> statement-breakpoint
ALTER TABLE "sessions" ADD COLUMN "google_refresh_token" text;--> statement-breakpoint
ALTER TABLE "sessions" ADD COLUMN "expires_at" date;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "google_refresh_token" text;--> statement-breakpoint
ALTER TABLE "sessions" DROP COLUMN IF EXISTS "google_code";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN IF EXISTS "google_id";

View File

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "channel_id" text;

View File

@ -1 +0,0 @@
ALTER TABLE "users" DROP COLUMN IF EXISTS "google_refresh_token";

View File

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "google_id" text;

View File

@ -1 +0,0 @@
ALTER TABLE "users" ADD COLUMN "uploads_playlist_id" text;

View File

@ -1 +0,0 @@
ALTER TABLE "sessions" ALTER COLUMN "expires_at" SET DATA TYPE timestamp;

View File

@ -1,178 +1,35 @@
{ {
"id": "b6cb5336-be20-4e96-861f-fff166b013c7",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5", "version": "5",
"dialect": "pg", "dialect": "pg",
"id": "478200dd-a614-41dd-9cd1-f6368405417e",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_code": {
"name": "google_code",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": { "users": {
"name": "users", "name": "users",
"schema": "", "schema": "",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "uuid", "type": "serial",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true
"default": "gen_random_uuid()"
}, },
"google_id": { "full_name": {
"name": "google_id", "name": "full_name",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"name": { "phone": {
"name": "name", "name": "phone",
"type": "text", "type": "varchar(256)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
} }
}, },
"indexes": {}, "indexes": {},
"foreignKeys": {}, "foreignKeys": {},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {}
"uniqueConstraints": {}
} }
}, },
"enums": {}, "enums": {},

View File

@ -1,197 +0,0 @@
{
"id": "de9d771e-4272-49d2-bcc7-6eb5291bcd51",
"prevId": "b6cb5336-be20-4e96-861f-fff166b013c7",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "date",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,203 +0,0 @@
{
"id": "ef238b19-05f5-4be9-90f8-56f4a08bb576",
"prevId": "de9d771e-4272-49d2-bcc7-6eb5291bcd51",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "date",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"channel_id": {
"name": "channel_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,197 +0,0 @@
{
"id": "d7ae7ea1-7218-4141-a9a9-35395e42e9bd",
"prevId": "ef238b19-05f5-4be9-90f8-56f4a08bb576",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "date",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"channel_id": {
"name": "channel_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,203 +0,0 @@
{
"id": "130129fa-d718-4be8-8ffe-f285dce85195",
"prevId": "d7ae7ea1-7218-4141-a9a9-35395e42e9bd",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "date",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"google_id": {
"name": "google_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"channel_id": {
"name": "channel_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,209 +0,0 @@
{
"id": "4c4cea1e-1ffe-4736-b045-7dadd7e22244",
"prevId": "130129fa-d718-4be8-8ffe-f285dce85195",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "date",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"google_id": {
"name": "google_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"channel_id": {
"name": "channel_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"uploads_playlist_id": {
"name": "uploads_playlist_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,209 +0,0 @@
{
"id": "3ec0762e-c27b-4b7f-b4a6-4db07400a5ae",
"prevId": "4c4cea1e-1ffe-4736-b045-7dadd7e22244",
"version": "5",
"dialect": "pg",
"tables": {
"articles": {
"name": "articles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"site_id": {
"name": "site_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_video_id": {
"name": "source_video_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"articles_site_id_sites_id_fk": {
"name": "articles_site_id_sites_id_fk",
"tableFrom": "articles",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"google_access_token": {
"name": "google_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_refresh_token": {
"name": "google_refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sites": {
"name": "sites",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"sites_user_id_users_id_fk": {
"name": "sites_user_id_users_id_fk",
"tableFrom": "sites",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"google_id": {
"name": "google_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"channel_id": {
"name": "channel_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"uploads_playlist_id": {
"name": "uploads_playlist_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -5,51 +5,9 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1713532681066, "when": 1686356934306,
"tag": "0000_huge_ares", "tag": "0000_plain_killmonger",
"breakpoints": true "breakpoints": false
},
{
"idx": 1,
"version": "5",
"when": 1713547912112,
"tag": "0001_slim_xavin",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713549800799,
"tag": "0002_small_forge",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713550262815,
"tag": "0003_elite_falcon",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1713550343102,
"tag": "0004_daffy_ironclad",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1713551541682,
"tag": "0005_tricky_sheva_callister",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1713614011867,
"tag": "0006_wide_the_hunter",
"breakpoints": true
} }
] ]
} }

View File

@ -1,31 +0,0 @@
import { date, foreignKey, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
google_id: text("google_id"),
name: text("name"),
email: text("email"),
channel_id: text("channel_id"),
uploads_playlist_id: text("uploads_playlist_id")
});
export const sessions = pgTable("sessions", {
id: uuid("id").defaultRandom().primaryKey(),
user_id: uuid("user_id").references(() => users.id),
access_token: text("google_access_token"),
refresh_token: text("google_refresh_token"),
expires_at: timestamp("expires_at")
})
export const site = pgTable("sites", {
id: uuid("id").defaultRandom().primaryKey(),
user_id: uuid("user_id").references(() => users.id),
name: text("name")
})
export const article = pgTable("articles", {
id: uuid("id").defaultRandom().primaryKey(),
site_id: uuid("site_id").references(() => site.id),
content: text("content"),
source_video_id: text("source_video_id")
})

7
src/db/schemas.ts Normal file
View File

@ -0,0 +1,7 @@
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
fullName: text("full_name"),
phone: varchar("phone", { length: 256 }),
});

21
src/fastify.d.ts vendored
View File

@ -1,21 +0,0 @@
import {OAuth2Namespace} from "@fastify/oauth2";
import type { db } from "./db";
import type { Redis } from "./utils";
declare module "fastify" {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface FastifyRequest {
// db: typeof db;
// redis: typeof Redis;
session?: {
id: string,
user_id: string,
google_access_token: string,
google_refresh_token: string,
expires_at: Date
}
}
interface FastifyInstance {
googleOAuth2: OAuth2Namespace
}
}

View File

@ -1,77 +0,0 @@
import { initDb } from "./db/index.js";
import { channelRoutes, authRoutes } from "./routes/index.js";
import { env, Logger, Redis } from "./utils/index.js";
import fastify from "fastify";
import { middleware } from "./modules/middleware.js";
import oauth from '@fastify/oauth2';
import { videoRoutes } from "./routes/videos.js";
const API_VERSION = "v1";
export const main = async () => {
const server = fastify({
bodyLimit: 1_000_000,
trustProxy: true,
});
await initDb();
// await Redis.initialize();
server.register(middleware);
server.register(import("@fastify/cors"), {
maxAge: 600,
origin: true,
credentials: true,
});
server.register(oauth, {
name: 'googleOAuth2',
scope: ['https://www.googleapis.com/auth/youtube.readonly', 'https://www.googleapis.com/auth/youtube.force-ssl', "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
credentials: {
client: {
id: '103003963446-6m80lqbe4bkgjv1nq5ihmju0bb6agkkg.apps.googleusercontent.com',
secret: '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'
},
auth: oauth.GOOGLE_CONFIGURATION
},
callbackUriParams: {
// custom query param that will be passed to callbackUri
access_type: 'offline', // will tell Google to send a refreshToken too
},
pkce: 'S256',
// register a fastify url to start the redirect flow
startRedirectPath: '/auth/google',
// facebook redirect here after the user login
callbackUri: 'http://localhost:3000/auth/google/callback'
})
// Routes
server.register(channelRoutes, {
prefix: `/`,
});
server.register(videoRoutes, {
prefix: `/videos`,
});
server.register(authRoutes, {
prefix: `/auth`,
});
server.get("/hello", (req, res) => {
res.send("world");
})
server.listen({ host: env.HOST, port: env.PORT }, (error, address) => {
if (error) {
Logger.error("INIT", error.message);
throw new Error(error.message);
}
Logger.info("INIT", `Server listening at ${address}`);
});
return server;
};
main();

40
src/index.ts Normal file
View File

@ -0,0 +1,40 @@
import { initDb } from "@api/db";
import { testRoutes } from "@api/routes";
import { env, Logger, Redis } from "@api/utils";
import fastify from "fastify";
const API_VERSION = "v1";
export const main = async () => {
const server = fastify({
bodyLimit: 1_000_000,
trustProxy: true,
});
await initDb();
await Redis.initialize();
server.register(import("@fastify/cors"), {
maxAge: 600,
origin: true,
credentials: true,
});
// Routes
server.register(testRoutes, {
prefix: `/${API_VERSION}/test`,
});
server.listen({ host: env.HOST, port: env.PORT }, (err, address) => {
if (err) {
Logger.error("INIT", err.message);
process.exit(1);
}
Logger.info("INIT", `Server listening at ${address}`);
});
return server;
};
main();

View File

@ -1,44 +0,0 @@
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
import fp from "fastify-plugin";
import { Redis } from "../utils/index.js";
import { db } from "../db/index.js";
import { sessions } from "../db/schemas.js";
import { eq } from "drizzle-orm";
const middleware = fp(
/**
* @param {FastifyInstance} fastify
* @param {unknown} _options
*/
async (fastify, _options) => {
fastify.addHook("onRequest", async (request) => {
// request.redis = Redis;
// request.db = db;
});
});
const authMiddleware = fp(
async (fastify, _options) => {
fastify.addHook("preValidation", async (request, response) => {
try {
if (!request.headers.authorization || !request.headers.authorization.startsWith("Bearer")) {
response.status(401).send("Missing authentication token");
return;
}
const token = request.headers.authorization.split(" ")[1];
const session = await db.select().from(sessions).where(eq(sessions.id, token));
if(session.length == 0) {
response.status(401).send("Invalid authentication token");
return;
}
// console.log(token);
// console.log(session);
request.session = session[0];
} catch (e) {
console.log(e);
}
});
}
)
export { middleware, authMiddleware };

View File

@ -1,68 +0,0 @@
import sget from 'simple-get';
import { createAuthToken as createSession } from '../utils/token.js';
import { google } from 'googleapis';
import { getChannelInfo, getUserInfo } from '../utils/youtube.js';
import { db } from '../db/index.js';
import { users as usersTable } from '../db/schemas.js';
import { userInfo } from 'os';
import { eq } from 'drizzle-orm';
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
/**
*
* @param {FastifyInstance} fastify
* @param {unknown} _
* @param {() => void} done
*/
export const authRoutes = (fastify, _, done) => {
fastify.get("/google/callback", async (request, response) => {
try {
const { token } = await fastify.googleOAuth2.getAccessTokenFromAuthorizationCodeFlow(request);
const user_info = await getUserInfo(token.access_token);
if (!user_info.verified_email) {
response.status(400).send({ success: false, message: "Provider email is not verified" });
return;
}
const channel_info = await getChannelInfo(token.access_token);
const existing_user = await db.select().from(usersTable).where(eq(usersTable.google_id, user_info.id)).then(x=>x[0]);
let user;
if (existing_user) {
user = existing_user;
}
else {
user = await db.insert(usersTable).values({
name: user_info.name,
google_id: user_info.id,
email: user_info.email,
channel_id: channel_info.id,
uploads_playlist_id: channel_info.contentDetails.relatedPlaylists.uploads
}).returning({ id: usersTable.id }).then(x=>x[0]);
if (user.length == 0) {
response.status(400).send({ success: false, message: "Problem when creating user account" });
return;
}
}
const session = await createSession(user.id, {
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at: new Date(token.expires_at)
});
response.send({
token: session.session_id
})
return;
} catch (e) {
console.log(e);
response.send({ success: false, message: e.message });
return;
}
});
done();
};

View File

@ -1,36 +0,0 @@
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
import { authMiddleware } from "../modules/middleware.js";
import { getChannelInfo } from "../utils/youtube.js";
/**
*
* @param {FastifyInstance} fastify
* @param {unknown} _
* @param {() => void} done
*/
export const channelRoutes = (fastify, _, done) => {
fastify.register(authMiddleware);
fastify.get("/", async (request, response) => {
try {
console.log(request.session);
const { token } = await fastify.googleOAuth2.getNewAccessTokenUsingRefreshToken({
refresh_token: request.session.google_refresh_token,
expires_at: request.session.expires_at
});
console.log(token);
const channel = await getChannelInfo(token.access_token);
response.send({
success: true,
channel
})
} catch (e) {
console.log(e);
}
});
done();
};

View File

@ -1,2 +0,0 @@
export * from "./channels.js";
export * from "./auth.js";

1
src/routes/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./test";

11
src/routes/test.ts Normal file
View File

@ -0,0 +1,11 @@
import type { FastifyInstance } from "fastify";
export const testRoutes = (fastify: FastifyInstance, _: unknown, done: () => void) => {
fastify.get("/", async (_req, res) => {
res.send({
hello: "world",
});
});
done();
};

View File

@ -1,61 +0,0 @@
/** @typedef {import("fastify").FastifyInstance} FastifyInstance */
import { eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { users } from "../db/schemas.js";
import { authMiddleware } from "../modules/middleware.js";
import { getCaptionText, getNewToken, getVideoCaptions, getVideosFromPlaylist } from "../utils/youtube.js";
/**
*
* @param {FastifyInstance} fastify
* @param {unknown} _
* @param {() => void} done
*/
export const videoRoutes = (fastify, _, done) => {
fastify.register(authMiddleware);
fastify.get("/", async (request, response) => {
try {
const token = await getNewToken(fastify.googleOAuth2, request.session, {});
const [user] = await db.select().from(users).where(eq(users.id, request.session.user_id));
const videos = await getVideosFromPlaylist(token.access_token, user.uploads_playlist_id);
response.send({
success: true,
videos
})
} catch (e) {
console.log(e);
}
});
fastify.get("/captions/:video_id", async (request, response) => {
try {
const token = await getNewToken(fastify.googleOAuth2, request.session, {});
const captions_list = await getVideoCaptions(token.access_token, request.params.video_id);
const caption = captions_list.filter(x => x.snippet.language === "en");
if (caption.length === 0) {
response.send({
success: false,
message: "Couldn't find caption"
});
return;
}
const caption_text = await getCaptionText(token.access_token, caption[0].id);
response.send({
captions_info: caption,
captions: caption_text
});
} catch (e) {
console.log(e);
}
})
done();
};

View File

@ -1,11 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import "dotenv/config"; import "dotenv/config";
const envSchema = z.object({ const env_schema = z.object({
DATABASE_URL: z.string().default("postgres://user:password@127.0.0.1:5432/postgres"), DATABASE_URL: z.string().default("postgres://user:password@127.0.0.1:5432/postgres"),
REDIS_URL: z.string().default("redis://127.0.0.1:6379/"), REDIS_URL: z.string().default("redis://127.0.0.1:6379/"),
PORT: z.coerce.number().default(8080), PORT: z.number().default(8080),
HOST: z.string().default("127.0.0.1"), HOST: z.string().default("127.0.0.1"),
}); });
export const env = envSchema.parse(process.env); export const env = env_schema.parse(process.env);

View File

@ -1,3 +0,0 @@
export * from "./env.js";
export * from "./logger.js";
export * from "./redis.js";

3
src/utils/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./env";
export * from "./logger";
export * from "./redis";

View File

@ -1,32 +0,0 @@
import clc from "cli-color";
class Logger {
/**
*
* @param {string} prefix
* @param {string} message
*/
static info(prefix, message) {
console.log(`[${clc.cyan(prefix)}] ${message}`);
}
/**
*
* @param {string} prefix
* @param {string} message
*/
static error(prefix, message) {
console.log(`[${clc.red(prefix)}] ${message}`);
}
/**
*
* @param {string} prefix
* @param {string} message
*/
static success(prefix, message) {
console.log(`[${clc.green(prefix)}] ${message}`);
}
}
export { Logger };

17
src/utils/logger.ts Normal file
View File

@ -0,0 +1,17 @@
import clc from "cli-color";
class Logger {
public static info(prefix: string, message: string) {
console.log(`[${clc.cyan(prefix)}] ${message}`);
}
public static error(prefix: string, message: string) {
console.log(`[${clc.red(prefix)}] ${message}`);
}
public static success(prefix: string, message: string) {
console.log(`[${clc.green(prefix)}] ${message}`);
}
}
export { Logger };

View File

@ -1,91 +0,0 @@
import * as redis from "redis";
import { env, Logger } from "./index.js";
/** @typedef {"session" | "visits"} RedisPrefixes */
/** @typedef {`${RedisPrefixes}:${string | number}` | RedisPrefixes} RedisKey */
class Redis {
/**
* @type {ReturnType<typeof redis.createClient>}
* @public
*/
static redis;
/** @public */
static async initialize() {
this.redis = redis.createClient({
url: env.REDIS_URL,
});
this.redis.on("error", (error) => {
Logger.error("INIT", "Failed to connect to redis " + String(error));
throw new Error("Failed to connect to redis");
});
this.redis.on("connect", () => {
Logger.info("INIT", "Connected to redis");
});
await this.redis.connect();
}
/**
* @param {RedisKey} key Key of the value to set
* @param {string} value Value to set
* @param {?redis.SetOptions} options Options for the value to set
* @public
*
* @example
* await Redis.set("session:user_xxx", new Date().toISOString());
* await Redis.set("visits", "1");
*/
static async set(key, value, options) {
await this.redis.set(key, value, options);
}
/**
*
* @param {RedisKey} key
* @public
*/
static async incr(key) {
await this.redis.incr(key);
}
/**
*
* @param {RedisKey} key
* @public
*/
static async decr(key) {
await this.redis.decr(key);
}
/**
* @template T
*
* @param {RedisKey} key Key of the value to get
* @example
* const value = await Redis.get("session:user_xxx");
* const visits = await Redis.get("visits");
*
* @returns {T | undefined}
*/
static async get(key) {
return this.redis.get(key);
}
/**
*
* @param {RedisKey} key Key of the value to delete
* @public
* @example
* await Redis.del("session:user_xxx");
* await Redis.del("visits");
*/
static async del(key) {
await this.redis.del(key);
}
}
export { Redis };

39
src/utils/redis.ts Normal file
View File

@ -0,0 +1,39 @@
import * as redis from "redis";
import { env, Logger } from "@api/utils";
type RedisPrefixes = "session";
class Redis {
public static redis: ReturnType<typeof redis.createClient>;
public static async initialize() {
this.redis = redis.createClient({
url: env.REDIS_URL,
});
this.redis.on("error", (err) => {
Logger.error("INIT", "Failed to connect to redis " + String(err));
process.exit(1);
});
this.redis.on("connect", () => {
Logger.info("INIT", "Connected to redis");
});
await this.redis.connect();
}
public static async set(type: RedisPrefixes, key: string, value: string, options?: redis.SetOptions) {
await this.redis.set(`${type}:${key}`, value, options);
}
public static async get<T>(type: RedisPrefixes, key: string) {
return this.redis.get(`${type}:${key}`) as T | undefined;
}
public static async del(type: RedisPrefixes, key: string) {
await this.redis.del(`${type}:${key}`);
}
}
export { Redis, type RedisPrefixes };

View File

@ -1,29 +0,0 @@
import { db } from "../db/index.js";
import { sessions } from "../db/schemas.js";
/**
*
* @param {string} user_id User to assign the auth token to
* @param {{access_token: string, refresh_token: string, expires_at: Date}} googleAuth
*
* @returns {Promise<{session_id: string}>} Auth token
*/
export async function createAuthToken(user_id, {
access_token,
refresh_token,
expires_at
}) {
const token = await db.insert(sessions).values({
user_id,
google_access_token: access_token,
google_refresh_token: refresh_token,
expires_at
}).returning({ id: sessions.id });
if(token.length == 0) {
throw new Error("Error creating auth token");
}
return {
session_id: token[0].id
}
}

View File

@ -1,112 +0,0 @@
import { db } from '../db/index.js';
import { sessions } from '../db/schemas.js';
import { google } from 'googleapis';
const service = google.youtube("v3");
/**
*
* @param {string} access_token
* @param {string} video_id
*
* @returns {Promise<import("googleapis").youtube_v3.Schema$Caption[]>}
*/
export async function getVideoCaptions(access_token, video_id) {
const captions = await service.captions.list({
access_token,
videoId: video_id,
part: "id,snippet"
}).then(x=>x.data.items);
return captions;
}
/**
*
* @param {string} access_token
* @param {string} caption_id
*
* @returns {Promise<string>}
*/
export async function getCaptionText(access_token, caption_id) {
const caption_text = await service.captions.download({
id: caption_id,
part: "snippet",
tfmt: "srt",
tlang: "en",
headers: {
"Authorization": "Bearer " + access_token
}
}).then(res=>res.data).then(x=>x.text());
console.log(caption_text)
return caption_text;
}
/**
*
* @param {string} access_token
* @param {string} playlist_id
*
* @returns {Promise<import("googleapis").youtube_v3.Schema$PlaylistItem[]>}
*/
export async function getVideosFromPlaylist(access_token, playlist_id) {
const videos = await service.playlistItems.list({
access_token,
part: "id,snippet",
playlistId: playlist_id,
maxResults: 10
}).then(res => res);
return videos.data.items;
}
/**
*
* @param {string} access_token
*
* @returns {Promise<import("googleapis").youtube_v3.Schema$Channel>}
*/
export async function getChannelInfo(access_token) {
const channel = await service.channels.list({
access_token,
part: "id,contentDetails,snippet",
mine: true,
maxResults: 1
}).then(res => res);
return channel.data.items[0];
}
/** @typedef {{name: string, email: string, isVerified: boolean}} UserInfo */
/**
*
* @param {string} access_token
*
* @returns {Promise<UserInfo>}
*/
export async function getUserInfo(access_token) {
const url = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
// const url = "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses";
const response = await fetch(url, {
headers: {
"Authorization": `Bearer ${access_token}`
}
})
const data = await response.json();
return data
}
/**
*
* @param {import('@fastify/oauth2').OAuth2Namespace} oauth
* @param {import('@fastify/oauth2').Token} session
*
* @returns {Promise<import('@fastify/oauth2').OAuth2Token>}
*/
export async function getNewToken(oauth, session) {
const {token} = await oauth.getNewAccessTokenUsingRefreshToken(session, {});
return token;
}

240
test.md
View File

@ -1,240 +0,0 @@
last week AMD launched their new
graphics cards and I already reviewed
the RX 7900 XTX so today I'm going to
talk about this RX 7900 XT now I did not
have a chance to test the reference card
but I do have this tough gaming model
from Asus and while I do think that AMD
has done a great job at creating
high-end graphics cards and bringing
some well needed competition to the
market I also think that there are
plenty of things that still need a bit
of work and that should be improved so
I'm going to take a bit of a different
approach today and I'm going to talk
about my issues with this 7900 XT let's
go this video is brought to you by
Corsair and their brand new Dominator
Platinum RGB memory these super fast
ddr5 6000 Hertz memory kits are
specifically made for ryzen 7000 series
CPUs they feature a stylish aluminum
heat spreader with dhx technology that
keeps them nice and cool under load
offering a smooth and stable performance
with a lot out of room for overclocking
and they come with 12 customizable
kapelix LEDs that you can control with
their IQ software and easily sync up
with your other coarser components check
them out using the links in the
description below one of the first and
most obvious issues I had with this card
is its name the RX 7900 XT sounds very
similar to the 7900 XTX even though when
we look at the specs you can see that
they are vastly different the XCX has
more cores higher clock speeds more
memory and a bigger memory bus than the
XT and I don't think that removing One X
at the end of the product name is enough
to distinguish and obviously lower tier
card from a higher tier one it just
causes a lot of confusion especially for
the less informed consumers that might
think that they are buying a very
similar product for a hundred dollars
less which is definitely not the case
here now Nvidia received a lot of
criticism for their 4086 16 gig and 4080
12 gig cards and AMD here is pretty much
doing the same thing the XT is more than
15 slower than the xdx and they should
have called it a 7800 XT or at least a
7900 without any X's just to make it a
bit more obvious to everyone and that
brings me to my next issue the relative
performance because the XT is priced so
closely to the XTX its performance
should also be at a close distance
compared to the xdx and that is
definitely not the case here so the xt's
recommended price is only 10 lower than
the xdx but when it comes to Performance
the XTX is 16 faster than the XD and
keep in mind I was using the relatively
fast third-party version of the XT so
for 100 more you can get a card that is
16 faster and if you already decided to
spend 900 on a GPU upgrading to dxdx for
a hundred dollars more just seems like a
logical choice and that pressure from
the relative performance is a bit of a
shame because in absolute terms the XT
is a pretty powerful chip at 1440p
resolution it shows a great High refresh
rate experience in every game I've
tested and most games will easily run at
144 FPS or above but even the heaviest
titles like Dying Light 2 and cyberpunk
2077 for example will get pretty close
to that at 4K resolution the XC does
well offering that 4K 120 experience in
lighter titles and 60-ish FPS or above
in heavier games amd's FSR has also been
adopted by a lot of game developers so
for the titles that do support it this
7900 XT is actually a very capable 4K
gaming card as well but should you buy a
much stronger XTX for 100 more
absolutely now the next issue I had was
performance inconsistency when testing
Call of Duty Modern Warfare we were
getting really inconsistent results so
one moment the XT would be running at
around 2400 megahertz showing FPS
numbers that kind of fall in line with
other cards but during some other runs
it would suddenly boost much higher
somewhere closer to 2 900 megahertz
which would result in a much higher
average FPS and which would put it above
the RTX 4090 and the 7900 XDS and there
was absolutely no logic behind it so it
was the same system same settings no
updates in between the CPU wasn't
throttling or anything else that we can
think of actually so every time we
reboot the PC it would act differently
for no clear reason now in this
particular game it doesn't really matter
as much because in the worst case
scenario it was close to an RTX 4090 and
in the best case scenario it was
actually beating it which is definitely
not bad for a GPU that costs half as
much but seeing this inconsistent
Behavior always causes doubt whether
everything is working well as it should
and what kind of an impact does it have
in titles where this card was not doing
as well but AMD has been on top of this
since I mentioned it and they are trying
to figure it out but there are no clear
answers just yet now hopefully they will
come up with a driver update that will
stabilize this situation a bit more now
my next issue is a really noticeable one
coil line in my review of the XTX I
mentioned that my reference card had
some coil line now coil wine is mostly
sample specific and then it is often
specific to certain games or super high
FPS gameplay but the XT here had a
noticeable amount of coal wine regularly
and since the launch a lot of reviewers
as well as people who bought their cards
frequently mentioned coil wine too both
in reference cards as well as third
party cards so this tough gaming from
Asus doesn't always whine but it does
sit pretty frequently and since the
sound often changes depending on what's
going on with your PC it is way more
noticeable than if it was just a
consistent tone I did manage to test the
thermal and noise performance at one
point where it wasn't whining and this
oversized tough cooler performed
extremely well barely going over 50
degrees with almost no noise at all and
even though this should be a great
cooler design it is only quiet sometimes
the other times it is just super whiny
and given the fact that these reports
are so widespread with all sorts of
Radeon cards it just feels like this is
something that AMD really needs to work
on my next issue is the power
consumption especially in idle when
having multiple monitors connected to
your cart if I just connect a single
monitor power consumption is completely
fine with a single watt of a difference
between the 4080 and this 7900 XD but
when connecting two monitors so I had
one 4K 120 hertz one and one 4K 60hz one
the power consumption on the XT went up
by a lot it was 15 watts higher than the
4080 and I would say that is a
significant change now depending on how
much you pay for your electricity and
how many hours you have your monitors on
per day this could really add up quickly
now supposedly this should get fixed
with the next driver so I will put an
update in the comment under this video
once I learn more about this
but perhaps the biggest issue right now
is the supply and pricing before these
cards launch there were plenty of posts
saying that there should be plenty of
stock and that the prices should be as
promised but in reality the supply so
far hasn't been as good it will vary per
region but if I look at the Netherlands
I haven't seen a single xdx listed at
any reputable retailer and even the XT
which had more critical reviews overall
has been really hard to find most shops
I spoke to told me that they have no
clear indication of stock on any Radeon
cards at this point and the couple shops
that did list the XT seem to have very
limited stock and they're selling them
at very high prices so right now the
cheapest one I can find is at 1300 Euros
which is 250 euros over the MSRP and
only about 150 Euros less than a 4080
and a 4080 is faster has stronger Ray
tracing performance and it is more power
efficient and I think all this was much
worse because of the hype that followed
the AMD launch and the belief that there
might actually be some stock or that the
prices might actually stay down anyway
the launch was definitely a bit shaky
but it is important to remember that
most of the things I mentioned today are
absolutely fixable and that was the
reason of making this video because I
know know that AMD cares about criticism
and that they do want to fix things for
the end users at least the things that
can be fixed because as I said before
the absolute performance of this 7900 XT
is quite good and the only way to fix
their relative performance is to lower
the price because if it gets
significantly cheaper I'm sure that most
of us just would not care about the
confusing name business or the fact that
it is boosting a bit higher one day and
then a bit lower the next now the coil
wine is a bit more complicated to fix
especially for the cars that already
love the factory but for future batches
that too is definitely fixable and they
definitely should fix it because the GPU
Market is so competitive and even a
single downside like coil wine for
example can make a big difference at the
end of the day now that is all I have
for today I hope this video was
interesting enough let me know in the
comments down below what are your
thoughts about the XD and if you want to
make sure not to miss any of my future
videos try to click that subscribe
button bye guys and see in the next one

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ESNext",
"strict": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {
"@api/*": ["src/*"]
}
}
}

3608
yarn.lock

File diff suppressed because it is too large Load Diff