Burn it all to the ground and start with bun and a reorg
This commit is contained in:
23
src/server/api/root.ts
Normal file
23
src/server/api/root.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { photosRouter } from "./routers/photos/photos";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
* All routers added in /api/routers should be manually added here.
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
photos: photosRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
/**
|
||||
* Create a server-side caller for the tRPC API.
|
||||
* @example
|
||||
* const trpc = createCaller(createContext);
|
||||
* const res = await trpc.post.all();
|
||||
* ^? Post[]
|
||||
*/
|
||||
export const createCaller = createCallerFactory(appRouter);
|
||||
56
src/server/api/routers/photos/list/index.ts
Normal file
56
src/server/api/routers/photos/list/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { db } from "@/server/db";
|
||||
import { photos } from "@/server/db/schema";
|
||||
import { shake } from "radash";
|
||||
|
||||
export type ImageData = {
|
||||
width: number;
|
||||
height: number;
|
||||
blur: `data:image/${string}`;
|
||||
src: string;
|
||||
camera?: string;
|
||||
exif: Partial<{
|
||||
exposureBiasValue: number;
|
||||
fNumber: number;
|
||||
isoSpeedRatings: number;
|
||||
focalLength: number;
|
||||
takenAt: Date;
|
||||
LensModel: string;
|
||||
}>;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type ListOptions = {
|
||||
cursor: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export async function list(options: ListOptions): Promise<ImageData[]> {
|
||||
const currentSources = await db
|
||||
.select()
|
||||
.from(photos)
|
||||
.limit(options.limit)
|
||||
.offset(options.cursor);
|
||||
|
||||
const images = currentSources.map((photo) => {
|
||||
return {
|
||||
width: photo.width,
|
||||
height: photo.height,
|
||||
blur: `data:image/jpeg;base64,${photo.blur.toString("base64")}` as `data:image/${string}`,
|
||||
src: photo.src,
|
||||
camera: photo.camera ?? undefined,
|
||||
exif: shake({
|
||||
exposureBiasValue: photo.exposureBiasValue,
|
||||
fNumber: photo.fNumber,
|
||||
isoSpeedRatings: photo.isoSpeedRatings,
|
||||
focalLength: photo.focalLength,
|
||||
takenAt: photo.takenAt,
|
||||
lensModel: photo.lensModel,
|
||||
}),
|
||||
title: photo.title ?? undefined,
|
||||
description: photo.description ?? undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return images;
|
||||
}
|
||||
40
src/server/api/routers/photos/photos.ts
Normal file
40
src/server/api/routers/photos/photos.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { list } from "./list";
|
||||
import { update } from "./update";
|
||||
|
||||
export const photosRouter = createTRPCRouter({
|
||||
list: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
limit: z.number().nonnegative().default(1),
|
||||
cursor: z.number().nonnegative().default(0),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const ret = await list({
|
||||
limit: input.limit + 1,
|
||||
cursor: input.cursor,
|
||||
});
|
||||
|
||||
let next: number | undefined;
|
||||
if (ret.length > input.limit) {
|
||||
next = input.limit;
|
||||
ret.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
data: ret,
|
||||
next,
|
||||
};
|
||||
}),
|
||||
update: protectedProcedure.query(update),
|
||||
});
|
||||
90
src/server/api/routers/photos/update/index.ts
Normal file
90
src/server/api/routers/photos/update/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
GetObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import exifReader from "exif-reader";
|
||||
import { diff, sift } from "radash";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import { photos } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export async function update(): Promise<string[]> {
|
||||
const allPhotos = await db.select().from(photos);
|
||||
const currentSources = allPhotos.map((photo) => photo.src);
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region: "auto",
|
||||
endpoint: "https://fly.storage.tigris.dev",
|
||||
});
|
||||
|
||||
const listObjCmd = new ListObjectsV2Command({
|
||||
Bucket: "joemonk-photos",
|
||||
});
|
||||
|
||||
const s3Res = await s3Client.send(listObjCmd);
|
||||
|
||||
if (!s3Res.Contents) {
|
||||
throw new TRPCError({
|
||||
code: "GATEWAY_TIMEOUT",
|
||||
message: "Could not get contents from Tigris",
|
||||
});
|
||||
}
|
||||
const s3Photos = sift(
|
||||
s3Res.Contents.map((obj) => {
|
||||
if (!obj.Key?.endsWith("/")) {
|
||||
return `https://fly.storage.tigris.dev/joemonk-photos/${obj.Key}`;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
|
||||
const newPhotos = diff(s3Photos, currentSources);
|
||||
|
||||
if (newPhotos.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imageData = newPhotos.map(async (fileName: string) => {
|
||||
const getImageCmd = new GetObjectCommand({
|
||||
Bucket: "joemonk-photos",
|
||||
Key: fileName.replace(
|
||||
"https://fly.storage.tigris.dev/joemonk-photos/",
|
||||
"",
|
||||
),
|
||||
});
|
||||
const imgRes = await s3Client.send(getImageCmd);
|
||||
const image = await imgRes.Body?.transformToByteArray();
|
||||
|
||||
const { width, height, exif } = await sharp(image).metadata();
|
||||
const blur = await sharp(image)
|
||||
.resize({ width: 12, height: 12, fit: "inside" })
|
||||
.toBuffer();
|
||||
const exifData = exif ? exifReader(exif) : undefined;
|
||||
|
||||
const photo: typeof photos.$inferInsert = {
|
||||
src: fileName,
|
||||
width: width ?? 10,
|
||||
height: height ?? 10,
|
||||
blur: blur,
|
||||
camera: exifData?.Image?.Model ?? null,
|
||||
|
||||
exposureBiasValue: exifData?.Photo?.ExposureBiasValue ?? null,
|
||||
fNumber: exifData?.Photo?.FNumber ?? null,
|
||||
isoSpeedRatings: exifData?.Photo?.ISOSpeedRatings ?? null,
|
||||
focalLength: exifData?.Photo?.FocalLength ?? null,
|
||||
takenAt: exifData?.Photo?.DateTimeOriginal ?? null,
|
||||
lensModel: exifData?.Photo?.LensModel ?? null,
|
||||
};
|
||||
|
||||
return photo;
|
||||
});
|
||||
|
||||
const images = await Promise.all(imageData);
|
||||
|
||||
await db.insert(photos).values(images);
|
||||
|
||||
return newPhotos;
|
||||
}
|
||||
133
src/server/api/trpc.ts
Normal file
133
src/server/api/trpc.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||
* 1. You want to modify request context (see Part 1).
|
||||
* 2. You want to create a new middleware or type of procedure (see Part 3).
|
||||
*
|
||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||
* need to use are documented accordingly near the end.
|
||||
*/
|
||||
|
||||
import { TRPCError, initTRPC } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { auth } from "@/server/auth";
|
||||
import { db } from "@/server/db";
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
*
|
||||
* This section defines the "contexts" that are available in the backend API.
|
||||
*
|
||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||
*
|
||||
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
|
||||
* wrap this and provides the required context.
|
||||
*
|
||||
* @see https://trpc.io/docs/server/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
||||
const session = await auth();
|
||||
|
||||
return {
|
||||
db,
|
||||
session,
|
||||
...opts,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
|
||||
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
||||
* errors on the backend.
|
||||
*/
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError:
|
||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a server-side caller.
|
||||
*
|
||||
* @see https://trpc.io/docs/server/server-side-calls
|
||||
*/
|
||||
export const createCallerFactory = t.createCallerFactory;
|
||||
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
*
|
||||
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
||||
* "/src/server/api/routers" directory.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is how you create new routers and sub-routers in your tRPC API.
|
||||
*
|
||||
* @see https://trpc.io/docs/router
|
||||
*/
|
||||
export const createTRPCRouter = t.router;
|
||||
|
||||
/**
|
||||
* Middleware for timing procedure execution and adding an artificial delay in development.
|
||||
*
|
||||
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
|
||||
* network latency that would occur in production but not in local development.
|
||||
*/
|
||||
const timingMiddleware = t.middleware(async ({ next, path }) => {
|
||||
const start = Date.now();
|
||||
|
||||
if (t._config.isDev) {
|
||||
// artificial delay in dev
|
||||
const waitMs = Math.floor(Math.random() * 400) + 100;
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
|
||||
const result = await next();
|
||||
|
||||
const end = Date.now();
|
||||
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Public (unauthenticated) procedure
|
||||
*
|
||||
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
|
||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||
* are logged in.
|
||||
*/
|
||||
export const publicProcedure = t.procedure.use(timingMiddleware);
|
||||
|
||||
/**
|
||||
* Protected (authenticated) procedure
|
||||
*
|
||||
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
||||
* the session is valid and guarantees `ctx.session.user` is not null.
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure
|
||||
.use(timingMiddleware)
|
||||
.use(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
53
src/server/auth/config.ts
Normal file
53
src/server/auth/config.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { env } from "@/env";
|
||||
import { getBaseUrl } from "@/lib/base-url";
|
||||
import type { DefaultSession, NextAuthConfig } from "next-auth";
|
||||
|
||||
/**
|
||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||
* object and keep type safety.
|
||||
*
|
||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
||||
*/
|
||||
declare module "next-auth" {
|
||||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
// interface User {
|
||||
// // ...other properties
|
||||
// // role: UserRole;
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
||||
*
|
||||
* @see https://next-auth.js.org/configuration/options
|
||||
*/
|
||||
export const authConfig = {
|
||||
providers: [
|
||||
{
|
||||
id: "authelia",
|
||||
name: "Authelia",
|
||||
type: "oidc",
|
||||
issuer: "https://auth.home.joemonk.co.uk",
|
||||
clientId: env.AUTH_CLIENT_ID,
|
||||
clientSecret: env.AUTH_CLIENT_SECRET,
|
||||
},
|
||||
],
|
||||
trustHost: true,
|
||||
redirectProxyUrl: `${getBaseUrl()}/api/auth`,
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
} satisfies NextAuthConfig;
|
||||
10
src/server/auth/index.ts
Normal file
10
src/server/auth/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { cache } from "react";
|
||||
|
||||
import { authConfig } from "./config";
|
||||
|
||||
const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
|
||||
|
||||
const auth = cache(uncachedAuth);
|
||||
|
||||
export { auth, handlers, signIn, signOut };
|
||||
19
src/server/db/index.ts
Normal file
19
src/server/db/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { type Client, createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
|
||||
import { env } from "@/env";
|
||||
import * as schema from "./schema";
|
||||
|
||||
/**
|
||||
* Cache the database connection in development. This avoids creating a new connection on every HMR
|
||||
* update.
|
||||
*/
|
||||
const globalForDb = globalThis as unknown as {
|
||||
client: Client | undefined;
|
||||
};
|
||||
|
||||
export const client =
|
||||
globalForDb.client ?? createClient({ url: env.DATABASE_URL });
|
||||
if (env.NODE_ENV !== "production") globalForDb.client = client;
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
20
src/server/db/schema.ts
Normal file
20
src/server/db/schema.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const photos = sqliteTable("photo", (d) => ({
|
||||
id: d.integer({ mode: "number" }).primaryKey({ autoIncrement: true }),
|
||||
|
||||
src: d.text({ length: 256 }).notNull().unique(),
|
||||
width: d.integer({ mode: "number" }).notNull(),
|
||||
height: d.integer({ mode: "number" }).notNull(),
|
||||
blur: d.blob({ mode: "buffer" }).notNull(),
|
||||
|
||||
camera: d.text({ length: 128 }),
|
||||
title: d.text({ length: 128 }),
|
||||
description: d.text({ length: 1024 }),
|
||||
exposureBiasValue: d.integer({ mode: "number" }),
|
||||
fNumber: d.real(),
|
||||
isoSpeedRatings: d.integer({ mode: "number" }),
|
||||
focalLength: d.integer({ mode: "number" }),
|
||||
takenAt: d.integer({ mode: "timestamp" }),
|
||||
lensModel: d.text({ length: 128 }),
|
||||
}));
|
||||
Reference in New Issue
Block a user