Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/persistence-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## Project Overview

This project is a custom-built mini-ORM utility for managing database operations in a Node.js/TypeScript application. It includes features like query building, pagination, joins, and CRUD operations.

### Key Directories and Files

- **`src/backend/persistence/`**: Contains the core persistence logic.

- `persistence.repository.ts`: Implements the `PersistentRepository` class for database operations.
- `persistence-contracts.ts`: Defines interfaces and types for persistence operations.
- `persistence-utils.ts`: Utility functions for building SQL queries.
- `persistence-where-operator.ts`: Helper functions for building `WHERE` clauses.
- `database-drivers/pg.client.ts`: PostgreSQL database driver implementation.

- **`src/backend/persistence-repositories.ts`**: Exports repository instances for specific database tables.

### Key Features

1. **Dynamic Query Building**:

- Supports `WHERE`, `ORDER BY`, `JOIN`, and pagination.
- Utility functions like `buildWhereClause`, `buildOrderByClause`, and `buildJoinClause`.

2. **CRUD Operations**:

- `findRows`, `findRowCount`, `createOne`, `createMany`, `updateOne`, `deleteRows`.

3. **Error Handling**:

- Custom error classes like `PersistenceDriverError`.

4. **Database Driver**:
- Uses PostgreSQL (`pg` package) for database interactions.

### Testing

- Tests are written using Jest.
- Mocking is used for the database driver (`IPersistentDriver`).
- Test cases cover all repository methods and utility functions.

### Common Queries

1. **How to test a specific method?**

- Provide the method name and its file path. Example: `findRows` in `persistence.repository.ts`.

2. **How to add a new feature?**

- Specify the feature and its purpose. Example: Add support for `GROUP BY` in queries.

3. **How to debug an issue?**

- Provide the error message and the relevant file or method.

4. **How to write migrations?**
- Specify the table and column changes. Example: Add a new column to the `users` table.

### Example Prompts

- "Can you check the `findRows` method in `persistence.repository.ts`?"
- "Can you write test cases for the `updateOne` method?"
- "How can I add a `GROUP BY` clause to the query builder?"
- "Can you help me debug this error: `Failed to initialize database connection`?"

### Notes

- Always use parameterized queries to prevent SQL injection.
- Follow TypeScript best practices for type safety.
- Use environment variables for database configuration.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/techdiary.dev.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 39 additions & 6 deletions src/app/(dashboard-editor)/dashboard/articles/[uuid]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
import ArticleEditor from "@/components/Editor/ArticleEditor";
import * as articleActions from "@/backend/services/article.actions";
import React from "react";
import { Article, ArticleTag, Tag, User } from "@/backend/models/domain-models";
import { persistenceRepository } from "@/backend/persistence-repositories";
import { DatabaseTableName } from "@/backend/persistence/persistence-contracts";
import {
and,
eq,
inArray,
leftJoin,
} from "@/backend/persistence/persistence-where-operator";
import * as sessionActions from "@/backend/services/session.actions";
import ArticleEditor from "@/components/Editor/ArticleEditor";
import { notFound } from "next/navigation";
import { persistenceRepository } from "@/backend/persistence-repositories";
import { eq, and } from "@/backend/persistence/persistence-where-operator";
import React from "react";

interface Props {
params: Promise<{ uuid: string }>;
}
const page: React.FC<Props> = async ({ params }) => {
const sessionUserId = await sessionActions.getSessionUserId();
const _params = await params;

// eq("author_id", sessionUserId)
const [article] = await persistenceRepository.article.findRows({
limit: 1,
where: and(eq("id", _params.uuid), eq("author_id", sessionUserId)),
joins: [
leftJoin<Article, User>({
as: "author",
joinTo: DatabaseTableName.users,
localField: "author_id",
foreignField: "id",
columns: ["id", "name", "username"],
}),
],
});

const aggregatedTags = await persistenceRepository.articleTag.findRows({
where: inArray("article_id", [article.id]),
joins: [
leftJoin<ArticleTag, Tag>({
as: "tag",
joinTo: "tags",
localField: "tag_id",
foreignField: "id",
columns: ["id", "name", "color", "icon", "description"],
}),
],
});

const tags = aggregatedTags?.map((item) => item?.tag);
if (tags.length) {
article.tags = tags as Tag[];
}

if (!article) {
throw notFound();
}
Expand Down
7 changes: 2 additions & 5 deletions src/app/[username]/[articleHandle]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Article, User } from "@/backend/models/domain-models";
import { persistenceRepository } from "@/backend/persistence-repositories";
import {
eq,
joinTable,
} from "@/backend/persistence/persistence-where-operator";
import { eq, leftJoin } from "@/backend/persistence/persistence-where-operator";
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
Expand Down Expand Up @@ -33,7 +30,7 @@ export default async function Image(options: ArticlePageProps) {
columns: ["title", "excerpt", "cover_image", "body"],
limit: 1,
joins: [
joinTable<Article, User>({
leftJoin<Article, User>({
as: "user",
joinTo: "users",
localField: "author_id",
Expand Down
39 changes: 32 additions & 7 deletions src/app/api/play/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import * as articleActions from "@/backend/services/article.actions";
import { NextResponse } from "next/server";
import {persistenceRepository} from "@/backend/persistence-repositories";
import {NextResponse} from "next/server";

export async function GET(request: Request) {
// return NextResponse.json({
// handle: await articleActions.updateArticle({
// article_id: "5f0a1c0c-a8c8-4f5b-b8d8-c4d4d4d4d4d4",
// }),
// });
// [
// {
// "key": "article_id",
// "operator": "=",
// "value": "317eb5cf-9ef5-4ef1-9da7-78007dd83149"
// },
// {
// "key": "tag_id",
// "operator": "not in",
// "value": []
// }
// ]
return NextResponse.json({
handle: await persistenceRepository.articleTag.deleteRows({
where: {
AND: [
{
"key": "article_id",
"operator": "=",
"value": "317eb5cf-9ef5-4ef1-9da7-78007dd83149"
},
{
"key": "tag_id",
"operator": "not in",
"value": ["060f882f-e40e-415b-bc06-ed618f77d9bc", "2e27c4b0-226d-41ed-ae3f-3f9ac493b6a7"]
}
]
}
}),
});
}
4 changes: 2 additions & 2 deletions src/app/sitemaps/articles/sitemap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { persistenceRepository } from "@/backend/persistence-repositories";
import {
and,
eq,
joinTable,
leftJoin,
neq,
} from "@/backend/persistence/persistence-where-operator";
import type { MetadataRoute } from "next";
Expand All @@ -14,7 +14,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
columns: ["handle", "updated_at"],
limit: -1,
joins: [
joinTable<Article, User>({
leftJoin<Article, User>({
as: "user",
joinTo: "users",
localField: "author_id",
Expand Down
4 changes: 4 additions & 0 deletions src/backend/models/domain-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,8 @@ export interface ArticleTag {
tag_id: string;
created_at: Date;
updated_at: Date;

// Relationships
article?: Article;
tag?: Tag;
}
14 changes: 11 additions & 3 deletions src/backend/persistence/persistence-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,25 @@ export type CompositeWhere<T> = {
};
export type WhereCondition<T> = SimpleWhere<T> | CompositeWhere<T>;

export interface IPersistenceJoin {
export interface IPersistenceLeftJoin {
as: string;
joinTo: string;
localField: string;
foreignField: string;
columns: string[];
}

export interface IPersistenceManyToManyJoin {
as: string;
pivotTable: DatabaseTableName;
localField: string;
foreignField: string;
columns: string[];
}

export interface IPersistentPaginationPayload<T> {
where?: WhereCondition<T>;
joins?: IPersistenceJoin[];
joins?: IPersistenceLeftJoin[];
orderBy?: Array<IPersistentOrderBy<T>>;
columns?: Array<keyof T>;
limit?: number;
Expand All @@ -88,7 +96,7 @@ export interface IPagination<T> {
where?: WhereCondition<T>; // No longer allows arrays
columns?: Array<keyof T>;
orderBy?: Array<IPersistentOrderBy<T>>;
joins?: IPersistenceJoin[];
joins?: IPersistenceLeftJoin[];
}

//------------------------------------
Expand Down
Loading