Compare commits

...

2 Commits

Author SHA1 Message Date
24c184a19f Created all initial routes for entries data entity 2026-02-02 17:49:05 -05:00
7bd0bbe2d8 Progress on API routes 2026-02-02 03:35:41 -05:00
16 changed files with 1036 additions and 99 deletions

View File

@@ -1,29 +1,9 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
# Glossary API
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
## Stack
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
- [Nest](https://github.com/nestjs/nest)
- TypeORM
## Project setup
@@ -31,9 +11,18 @@
$ npm install
```
## Running local infrastructure
To simplify development, all local infrastructure is handled with Docker. The folder `dev_infra` contains the folloeing:
- `compose.yaml`: A Docker compose file that stands up a Postgres database and a pgAdmin instance to visualize.
## Compile and run the project
```bash
# Set env variables
$ . ./set-env
# development
$ npm run start
@@ -87,12 +76,6 @@ Check out a few resources that may come in handy when working with NestJS:
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

31
dev_infra/compose.yaml Normal file
View File

@@ -0,0 +1,31 @@
services:
# PostgreSQL database
postgresql_db:
image: postgres:latest
container_name: postgresql-db
restart: always
ports:
- "5432:5432" # Default PostgreSQL port
environment:
POSTGRES_USER: glossary_dev_user
POSTGRES_DB: glossary_dev_db
POSTGRES_PASSWORD: glossary_dev_password
volumes:
- postgresql_db_data:/var/lib/postgresql # Docker managed volume
# pgAdmin4 Web UI for PostgreSQL database
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
restart: always
ports:
- "8888:80" # Expose Web UI to port 8888
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: postgresql
volumes:
- pgadmin_data:/var/lib/pgadmin # Docker managed volume
volumes:
postgresql_db_data:
pgadmin_data:

747
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,14 +17,26 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
"db:gen_migration": "ts-node ./node_modules/typeorm/cli.js migration:generate src/migrations/migration -d src/appDataSource.ts",
"db:gen_migration_named": "ts-node ./node_modules/typeorm/cli.js migration:generate src/migrations/$npm_config_name -d src/appDataSource.ts",
"db:show_migrations": "ts-node ./node_modules/typeorm/cli.js migration:show -d src/appDataSource.ts",
"db:run_migrations": "ts-node ./node_modules/typeorm/cli.js migration:run -d src/appDataSource.ts",
"db:run_migrations_prod": "ts-node ./node_modules/typeorm/cli.js migration:run -d dist/appDataSource.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"pg": "^8.18.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

8
set-env.sh Normal file
View File

@@ -0,0 +1,8 @@
export DB_TYPE=postgres
export DB_HOST=localhost
export DB_PORT=5432
export DB_USERNAME=glossary_dev_user
export DB_PASSWORD=glossary_dev_password
export DB_NAME=glossary_dev_db
npm run db:run_migrations

View File

@@ -1,10 +1,25 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { dataSourceOptions } from './appDataSource';
import { EntryModule } from './entries/entries.module';
import { DataSource, EntityManager } from 'typeorm';
@Module({
imports: [],
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot(dataSourceOptions),
EntryModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
export class AppModule {
constructor(
private dataSource: DataSource,
private entityManager: EntityManager,
) {}
}

18
src/appDataSource.ts Normal file
View File

@@ -0,0 +1,18 @@
import { DataSource, DataSourceOptions } from "typeorm";
import { Entry } from "./entries/entries.entity";
import { Initial1770020781006 } from "./migrations/1770020781006-initial";
import { AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 } from "./migrations/1770067407619-add_create_update_delete_cols_and_rename_source_to_sourceText";
export const dataSourceOptions: DataSourceOptions = {
type: "postgres",
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT as string, 10) || 5432,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [Entry],
migrations: [Initial1770020781006, AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619],
synchronize: false,
}
export const appDataSource = new DataSource(dataSourceOptions)

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EntriesController } from './entries.controller';
describe('EntriesController', () => {
let controller: EntriesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EntriesController],
}).compile();
controller = module.get<EntriesController>(EntriesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,50 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
import { EntryService } from './entries.service';
import { EntryDTO } from './entries.dto';
import { Entry } from './entries.entity';
@Controller('entries')
export class EntriesController {
constructor(
@Inject(EntryService)
private readonly entryService: EntryService
) {}
@Post()
async saveEntry(@Body() entry: EntryDTO): Promise<string | undefined> {
return (await this.entryService.save(entry)).uuid
}
@Put(":uuid")
async updateEntry(@Param("uuid") uuid: string, @Body() entry: EntryDTO): Promise<string | undefined> {
return (await this.entryService.updateByUuid(uuid, entry)).uuid
}
@Get()
async findAll(): Promise<Entry[]>
{
const entries = await this.entryService.findAll()
return entries.sort((a, b) => {
return (
new Date(b.created_at as Date).getTime() - new Date(a.created_at as Date).getTime()
)
})
}
@Get(":uuid")
async findOneByUuid(@Param("uuid") uuid: string): Promise<Entry | null> {
return await this.entryService.findOneByUuid(uuid)
}
@Delete(":uuid")
async softDelete(@Param("uuid") uuid: string): Promise<void>
{
await this.entryService.softDeleteByUuid(uuid)
}
@Put("/restore/:uuid")
async restoreSoftDeleted(@Param("uuid") uuid: string): Promise<Entry | null> {
return await this.entryService.restoreDeletedByUuid(uuid)
}
}

View File

@@ -0,0 +1,8 @@
import { Entry } from "./entries.entity";
export class EntryDTO implements Partial<Entry> {
title?: string;
description?: string;
sourceText?: string;
sourceUrl?: string;
}

View File

@@ -0,0 +1,32 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Generated, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity({ name: "entries"})
export class Entry {
@Column()
@Generated("uuid")
uuid?: string;
@PrimaryGeneratedColumn("increment")
id?: number;
@Column({ type: String, default: "" })
title?: string;
@Column({ type: String, default: "" })
description?: string;
@Column({ type: String, default: "" })
sourceText?: string;
@Column({ type: String, default: "" })
sourceUrl?: string;
@CreateDateColumn()
created_at?: Date;
@UpdateDateColumn()
updated_at?: Date;
@DeleteDateColumn()
deleted_at?: Date;
}

View File

@@ -0,0 +1,15 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Entry } from "./entries.entity";
import { EntryService } from "./entries.service";
import { EntriesController } from "./entries.controller";
@Module({
imports: [
TypeOrmModule.forFeature([Entry]),
],
exports: [EntryService],
providers: [EntryService],
controllers: [EntriesController],
})
export class EntryModule {}

View File

@@ -0,0 +1,85 @@
import { Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Entry } from "./entries.entity";
import { Repository } from "typeorm";
import { EntryDTO } from "./entries.dto";
import EntryNotFoundException from "./exceptions/entryNotFound.exception";
@Injectable()
export class EntryService {
constructor(
@InjectRepository(Entry)
private entryRepository: Repository<Entry>
) {}
private readonly logger = new Logger(EntryService.name)
async save(data: EntryDTO): Promise<Entry> {
this.logger.log("Creating entry")
const lEntry = new Entry();
lEntry.description = data.description
lEntry.sourceText = data.sourceText
lEntry.sourceUrl = data.sourceUrl
lEntry.title = data.title
await this.entryRepository.save(lEntry)
return lEntry
}
async updateByUuid(uuid: string, data: EntryDTO): Promise<Entry> {
this.logger.log(`Updating entry with UUID ${uuid}`)
const oldEntry = await this.entryRepository.findOneBy({ uuid: uuid})
if (!oldEntry) {
throw new EntryNotFoundException(uuid)
} else {
const lEntry = new Entry();
// `id` is the primary key
lEntry.id = oldEntry.id
// These options can be updated
lEntry.description = data.description
lEntry.sourceText = data.sourceText
lEntry.sourceUrl = data.sourceUrl
lEntry.title = data.title
await this.entryRepository.save(lEntry)
return lEntry
}
}
async findOneByUuid(uuid: string): Promise<Entry | null> {
this.logger.log(`Returning entry with UUID ${uuid}`)
return this.entryRepository.findOneByOrFail({uuid: uuid})
}
async findAll(): Promise<Entry[]> {
this.logger.log("Returning all entries")
return this.entryRepository.find()
}
async findAllAndDeleted(): Promise<Entry[]> {
this.logger.log("Returning all entries, active and deleted")
return this.entryRepository.find({ withDeleted: true })
}
async softDeleteByUuid(uuid: string): Promise<void> {
this.logger.log(`Soft deleting entry with UUID ${uuid}`)
const deleteResponse = await this.entryRepository.softDelete({
uuid: uuid
})
if (!deleteResponse.affected) {
throw new EntryNotFoundException(uuid)
}
}
async restoreDeletedByUuid(uuid: string): Promise<Entry | null> {
this.logger.log(`Restoring entry with UUID ${uuid}`)
const restoreResponse = await this.entryRepository.restore({
uuid: uuid
})
if (!restoreResponse.affected) {
throw new EntryNotFoundException(uuid)
} else {
return this.entryRepository.findOneByOrFail({
uuid: uuid
})
}
}
}

View File

@@ -0,0 +1,9 @@
import { NotFoundException } from "@nestjs/common";
class EntryNotFoundException extends NotFoundException {
constructor(uuid: string) {
super(`Entry with UUID ${uuid} not found`)
}
}
export default EntryNotFoundException

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Initial1770020781006 implements MigrationInterface {
name = 'Initial1770020781006'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "entries" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "id" SERIAL NOT NULL, "title" character varying NOT NULL DEFAULT '', "description" character varying NOT NULL DEFAULT '', "source" character varying NOT NULL DEFAULT '', "sourceUrl" character varying NOT NULL DEFAULT '', CONSTRAINT "PK_8640855ae82083455cbb806173d" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "entries"`);
}
}

View File

@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 implements MigrationInterface {
name = 'AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "source"`);
await queryRunner.query(`ALTER TABLE "entries" ADD "sourceText" character varying NOT NULL DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "entries" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "entries" ADD "updated_at" TIMESTAMP NOT NULL DEFAULT now()`);
await queryRunner.query(`ALTER TABLE "entries" ADD "deleted_at" TIMESTAMP`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "deleted_at"`);
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "updated_at"`);
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "created_at"`);
await queryRunner.query(`ALTER TABLE "entries" DROP COLUMN "sourceText"`);
await queryRunner.query(`ALTER TABLE "entries" ADD "source" character varying NOT NULL DEFAULT ''`);
}
}