Skip to content

Commit

Permalink
👔 Startup article server logic
Browse files Browse the repository at this point in the history
  • Loading branch information
KONFeature committed Feb 5, 2024
1 parent 15f0c28 commit 606427c
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# dependencies
/node_modules
**/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
Expand Down
Binary file modified bun.lockb
Binary file not shown.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
"@types/node": "^20",
"typescript": "^5"
},
"workspaces": ["packages/*"]
"workspaces": ["packages/*"],
"dependencies": {
"viem": "^2.7.3"
}
}
55 changes: 55 additions & 0 deletions packages/example/src/context/admin/action/authenticate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use server";

import type { AdminSession } from "@/type/AdminSession";
import { type SessionOptions, getIronSession } from "iron-session";
import { cookies } from "next/headers";

/**
* Options used to store the session in the cookies
*/
const sessionOptions: SessionOptions = {
password: process.env.SESSION_ENCRYPTION_KEY ?? "",
cookieName: "admin-session",
ttl: 60 * 60 * 24, // 1 day
cookieOptions: {
secure: true,
},
};

/**
* Get the full session from the cookies
*/
async function getFullSession() {
return await getIronSession<AdminSession>(cookies(), sessionOptions);
}

/**
* Check if the current user is admin
* - Admin role is used to add articles
*/
export async function isAdmin() {
const currentSession = await getFullSession();
return currentSession.isAdmin ?? false;
}

/**
* Try to login as an admin
* @param password
*/
export async function login(password: string) {
if (password !== process.env.ADMIN_PASSWORD) {
throw new Error("Invalid password");
}

const session = await getFullSession();
session.isAdmin = true;
await session.save();
}

/**
* Delete the current session
*/
export async function deleteSession() {
const session = await getFullSession();
session.destroy();
}
14 changes: 14 additions & 0 deletions packages/example/src/context/article/dto/ArticleDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Hex } from "viem";

export type ArticleDocument = Readonly<{
// The id of the article (an hex string representing a byte32 hash)
_id: Hex;
// The content id linked to this article
contentId: Hex;
// The title of the article
title: string;
// The description of the article
description?: string;
// The link to access this article
link: string;
}>;
8 changes: 8 additions & 0 deletions packages/example/src/context/article/dto/ContentDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Hex } from "viem";

export type ContentDocument = Readonly<{
// The id of the content (an hex string, representing a bigint)
_id: Hex;
// The title of the content
title: string;
}>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ArticleDocument } from "@/context/article/dto/ArticleDocument";
import { DI } from "@/context/common/di";
import { getMongoDb } from "@/context/common/mongoDb";
import type { Collection, WithId } from "mongodb";
import type { Hex } from "viem";

/**
* Repository used to access the article collection
*/
export class ArticleRepository {
constructor(private readonly collection: Collection<ArticleDocument>) {}

/**
* Get an article by its id
* @param id
*/
public async getById(
id: Hex
): Promise<WithId<ArticleDocument> | undefined> {
return this.collection.findOne({ _id: id });
}

/**
* Get multiple article by their ids
* @param ids
*/
public async getByIds(ids: Hex[]): Promise<WithId<ArticleDocument>[]> {
return this.collection.find({ _id: { $in: ids } }).toArray();
}

/**
* Create a new article
*/
public async create(article: ArticleDocument) {
const insertResult = await this.collection.insertOne(article);
if (!insertResult.acknowledged) {
throw new Error("Failed to insert test content");
}
return insertResult.insertedId;
}
}

export const getArticleRepository = DI.registerAndExposeGetter({
id: "ArticleRepository",
isAsync: true,
getter: async () => {
const db = await getMongoDb();
return new ArticleRepository(
db.collection<ArticleDocument>("articles")
);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ContentDocument } from "@/context/article/dto/ContentDocument";
import { DI } from "@/context/common/di";
import { getMongoDb } from "@/context/common/mongoDb";
import type { Collection } from "mongodb";
import type { Hex } from "viem";

/**
* Repository used to access the content collection
*/
export class ContentRepository {
constructor(private readonly collection: Collection<ContentDocument>) {}

/**
* Get a content by its id
* @param id
*/
public async getById(id: Hex) {
const content = await this.collection.findOne({ _id: id });
if (!content) {
throw new Error(`Content ${id} not found`);
}
return content;
}
}

export const getContentRepository = DI.registerAndExposeGetter({
id: "ContentRepository",
isAsync: true,
getter: async () => {
const db = await getMongoDb();
return new ContentRepository(
db.collection<ContentDocument>("contents")
);
},
});
134 changes: 134 additions & 0 deletions packages/example/src/context/common/di.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
type ServiceIdentifier = string;
type ServiceGetter<T> = () => T | Promise<T>;
type ServiceEntry<T> = {
getter: ServiceGetter<T>;
instance?: T;
};

type ConditionalServiceGetter<
Return,
Getter extends ServiceGetter<Return> = ServiceGetter<Return>,
> = ReturnType<Getter> extends Promise<Return>
? () => Promise<Return>
: () => Return;

/**
* Our service register
*/
const registry = new Map<ServiceIdentifier, ServiceEntry<unknown>>();

/**
* Function to register a new service
*/
const register = <T>(id: ServiceIdentifier, getter: ServiceGetter<T>) => {
if (registry.has(id)) {
throw new Error(`Service ${id} already registered.`);
}

registry.set(id, { getter });
};

/**
* Register a new dependency in our DI Container and return a direct getter
* @param p
*/
function registerAndExposeGetter<ValueType>(p: {
id: ServiceIdentifier;
getter: () => ValueType;
isAsync?: false;
}): () => ValueType;

/**
* Register a new dependency in our DI Container and return an async getter
* @param p
*/
function registerAndExposeGetter<ValueType>(p: {
id: ServiceIdentifier;
getter: () => Promise<ValueType>;
isAsync: true;
}): () => Promise<ValueType>;

/**
* Register a new dependency in our DI Container
* @param id
* @param getter
* @param isAsync
*/
function registerAndExposeGetter<
ValueType,
BuilderType extends ServiceGetter<ValueType> = ServiceGetter<ValueType>,
GetterType extends ConditionalServiceGetter<
ValueType,
BuilderType
> = ConditionalServiceGetter<ValueType, BuilderType>,
>({
id,
getter,
isAsync,
}: {
id: ServiceIdentifier;
getter: ServiceGetter<ValueType>;
isAsync?: boolean;
}): ConditionalServiceGetter<ValueType, BuilderType> {
register(id, getter);
return (
isAsync ? () => getAsync<ValueType>(id) : () => get<ValueType>(id)
) as GetterType;
}

/**
* Get a dependency from our DI Container
* @param id
*/
async function getAsync<T>(id: ServiceIdentifier): Promise<T> {
const entry = registry.get(id);

// If we don't have an instance, and no getter, throw
if (!entry) {
throw new Error("Service not found.");
}

let { instance } = entry;

// If we don't have an instance yet, try to build it
if (!instance) {
instance = await entry.getter();
registry.set(id, { getter: entry.getter, instance });
}
return instance as T;
}

/**
* Get a dependency from our DI Container
* @param id
*/
function get<T>(id: ServiceIdentifier): T {
const entry = registry.get(id);

// If we don't have an instance, and no getter, throw
if (!entry) {
throw new Error("Service not found.");
}

let { instance } = entry;

// If we don't have an instance yet, try to build it
if (!instance) {
instance = entry.getter();
if (instance instanceof Promise) {
throw new Error(
"Service needs to be built asynchronously. Use getAsync to build and get the service."
);
}
registry.set(id, { getter: entry.getter, instance });
}

return instance as T;
}

/**
* Our DI Container
*/
export const DI = {
registerAndExposeGetter,
};
21 changes: 21 additions & 0 deletions packages/example/src/context/common/mongoDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use server";

import { DI } from "@/context/common/di";
import { MongoClient } from "mongodb";

// Get the mongo db client
export const getMongoDb = DI.registerAndExposeGetter({
id: "Mongo",
isAsync: true,
getter: async () => {
// Get the mongo client
// TODO: Should use Config.MONGODB_FRAK_POC_URI instead, but next isn't happy about it
const client = new MongoClient(
process.env.MONGODB_FRAK_POC_URI as string
);
// Connect to the database
await client.connect();
// and then connect to the poc database
return client.db("poc");
},
});
3 changes: 3 additions & 0 deletions packages/example/src/type/AdminSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type AdminSession = {
isAdmin: true;
};

0 comments on commit 606427c

Please sign in to comment.