Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode/

node_modules/
dist/
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "code-challenge",
"version": "1.0.0",
"scripts": {
"problem4:test": "node --require ts-node/register --test src/problem4/main.test.ts",
"problem5:start": "yarn ts-node src/problem5/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^5.0.6",
"@types/node": "^25.6.0",
"ts-node": "^10.9.2",
"typescript": "^5.0.0"
},
"dependencies": {
"better-sqlite3": "^12.9.0",
"express": "^5.2.1",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.28",
"zod": "^4.3.6"
}
}
24 changes: 24 additions & 0 deletions src/problem4/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from './main';

const implementations: [string, (n: number) => number][] = [
['sum_to_n_a (iterative)', sum_to_n_a],
['sum_to_n_b (recursive)', sum_to_n_b],
['sum_to_n_c (formula)', sum_to_n_c],
];

for (const [name, fn] of implementations) {
describe(name, () => {
test('n = 1 returns 1', () => assert.equal(fn(1), 1));
test('n = 5 returns 15', () => assert.equal(fn(5), 15));
test('n = 10 returns 55', () => assert.equal(fn(10), 55));
test('n = 100 returns 5050', () => assert.equal(fn(100), 5050));
test('n = 0 returns 1 (sum 0..1)', () => assert.equal(fn(0), 1));
test('n = 2 returns 3', () => assert.equal(fn(2), 3));
test('n = 1000 returns 500500', () => assert.equal(fn(1000), 500500));
test('n = -1 returns 0 (sum -1..1)', () => assert.equal(fn(-1), 0));
test('n = -2 returns -2 (sum -2..1)', () => assert.equal(fn(-2), -2));
test('n = -3 returns -5 (sum -3..1)', () => assert.equal(fn(-3), -5));
});
}
33 changes: 33 additions & 0 deletions src/problem4/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Three implementations of summing integers from 1 to `n` (inclusive).
* Behavior for any integer `n`:
* - If n >= 1: sum = 1 + 2 + ... + n
* - If n < 1: sum = n + (n+1) + ... + 1
* Assumes result fits within Number.MAX_SAFE_INTEGER.
*/

/** Iterative approach (loop). Time: O(|n|), Space: O(1) */
export function sum_to_n_a(n: number): number {
if (n >= 1) {
let sum = 0;
for (let i = 1; i <= n; i++) sum += i;
return sum;
}
let sum = 0;
for (let i = n; i <= 1; i++) sum += i;
return sum;
}

/** Recursive approach. Time: O(|n|), Space: O(|n|) (call stack) */
export function sum_to_n_b(n: number): number {
if (n === 1) return 1;
if (n > 1) return n + sum_to_n_b(n - 1);
return n + sum_to_n_b(n + 1);
}

/** Formula approach (constant time). Time: O(1), Space: O(1) */
export function sum_to_n_c(n: number): number {
if (n >= 1) return (n * (n + 1)) / 2;
const count = 2 - n; // number of terms from n..1
return ((n + 1) * count) / 2;
}
1 change: 1 addition & 0 deletions src/problem5/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
database.sqlite
112 changes: 112 additions & 0 deletions src/problem5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Problem 5 — CRUD REST API

A backend REST API built with **ExpressJS**, **TypeORM**, and **SQLite** (via `better-sqlite3`) in TypeScript.

## Requirements

- Node.js >= 16
- Yarn (or npm)

## Setup & Run

```bash
# From the project root
yarn install

# Start the server
yarn problem5:start
```

The server starts on `http://localhost:3000` by default.
Override the port with the `PORT` environment variable:

```bash
PORT=8080 yarn problem5:start
```

The SQLite database file is created automatically at `src/problem5/database.sqlite` on first run.

---

## API Reference

### Resource schema

| Field | Type | Description |
|-------------|--------|--------------------------------------|
| id | number | Auto-generated primary key |
| name | string | Required. Name of the resource |
| description | string | Optional description |
| status | string | `active` (default) or `inactive` |
| createdAt | date | Auto-set on creation |
| updatedAt | date | Auto-updated on change |

---

### Endpoints

#### Create a resource
```bash
curl -X POST http://localhost:3000/resources \
-H "Content-Type: application/json" \
-d '{"name": "My resource", "description": "Some info", "status": "active"}'
```
**Response:** `201 Created`
```json
{ "id": 1, "name": "My resource", "description": "Some info", "status": "active", "createdAt": "...", "updatedAt": "..." }
```

---

#### List resources
```bash
curl "http://localhost:3000/resources?name=my&status=active&page=1&size=10"
```
Query params (all optional):
- `name` — partial-match filter on name
- `status` — exact-match filter on status (`active` or `inactive`)
- `page` — page number (default: 1)
- `size` — items per page, max 100 (default: 10)

**Response:** `200 OK`
```json
{ "data": [...], "total": 42, "page": 1, "limit": 10 }
```

---

#### Get a resource
```bash
curl http://localhost:3000/resources/1
```
**Response:** `200 OK` with the resource object, or `404` if not found.

---

#### Update a resource
```bash
curl -X PUT http://localhost:3000/resources/1 \
-H "Content-Type: application/json" \
-d '{"name": "Updated name", "status": "inactive"}'
```
All fields are optional — only provided fields are updated.
**Response:** `200 OK` with the updated resource, or `404` if not found.

---

#### Delete a resource
```bash
curl -X DELETE http://localhost:3000/resources/1
```
**Response:** `204 No Content`, or `404` if not found.

---

#### Health check
```bash
curl http://localhost:3000/health
```
**Response:** `200 OK`
```json
{ "status": "ok" }
```
8 changes: 8 additions & 0 deletions src/problem5/constants/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum HttpStatus {
Ok = 200,
Created = 201,
NoContent = 204,
BadRequest = 400,
NotFound = 404,
InternalServerError = 500,
}
112 changes: 112 additions & 0 deletions src/problem5/controllers/resource.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Request, Response } from 'express';
import { HttpStatus } from '../constants/http';
import { datasource } from '../datasource';
import { Resource } from '../entities';
import { CreateDTO, ModifyDTO, ResourceIdDTO, ListDTO } from '../dto/resource.dto';

export const create = async (req: Request, res: Response) => {
const parsed = CreateDTO.safeParse(req.body);
if (!parsed.success) {
res.status(HttpStatus.BadRequest).json({ error: parsed.error.issues[0]?.message });
return;
}

const repo = datasource.getRepository(Resource);
const resource = repo.create(parsed.data);
const saved = await repo.save(resource);
res.status(HttpStatus.Created).json(saved);
};

export const list = async (req: Request, res: Response) => {
const parsed = ListDTO.safeParse(req.query);
if (!parsed.success) {
res.status(HttpStatus.BadRequest).json({ error: parsed.error.issues[0]?.message });
return;
}

const { name, status, page, size } = parsed.data;
const qb = datasource.getRepository(Resource).createQueryBuilder('resource');

if (name) {
qb.andWhere('resource.name LIKE :name', { name: `%${name}%` });
}
if (status) {
qb.andWhere('resource.status = :status', { status });
}

qb.offset((page - 1) * size).limit(size);

const [items, total] = await qb.getManyAndCount();
res.json({ data: items, total, page, limit: size });
};

export const get = async (req: Request, res: Response) => {
const parsed = ResourceIdDTO.safeParse(req.params['id']);
if (!parsed.success) {
res.status(HttpStatus.BadRequest).json({ error: parsed.error.issues[0]?.message });
return;
}

const resource = await datasource.getRepository(Resource).findOneBy({ id: parsed.data });
if (!resource) {
res.status(HttpStatus.NotFound).json({ error: 'Resource not found' });
return;
}

res.json(resource);
};

export const modify = async (req: Request, res: Response) => {
const idParsed = ResourceIdDTO.safeParse(req.params['id']);
if (!idParsed.success) {
res.status(HttpStatus.BadRequest).json({ error: idParsed.error.issues[0]?.message });
return;
}

const parsed = ModifyDTO.safeParse(req.body);
if (!parsed.success) {
res.status(HttpStatus.BadRequest).json({ error: parsed.error.issues[0]?.message });
return;
}

const repo = datasource.getRepository(Resource);
const resource = await repo.findOneBy({ id: idParsed.data });

if (!resource) {
res.status(HttpStatus.NotFound).json({ error: 'Resource not found' });
return;
}

const { name, description, status } = parsed.data;
if (name !== undefined) {
resource.name = name;
}
if (description !== undefined) {
resource.description = description;
}
if (status !== undefined) {
resource.status = status;
}

const updated = await repo.save(resource);
res.json(updated);
};

export const remove = async (req: Request, res: Response) => {
const parsed = ResourceIdDTO.safeParse(req.params['id']);
if (!parsed.success) {
res.status(HttpStatus.BadRequest).json({ error: parsed.error.issues[0]?.message });
return;
}

const repo = datasource.getRepository(Resource);
const resource = await repo.findOneBy({ id: parsed.data });

if (!resource) {
res.status(HttpStatus.NotFound).json({ error: 'Resource not found' });
return;
}

await repo.remove(resource);
res.status(HttpStatus.NoContent).send();
};
11 changes: 11 additions & 0 deletions src/problem5/datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { Resource } from './entities';

export const datasource = new DataSource({
type: 'better-sqlite3',
database: 'src/problem5/database.sqlite',
synchronize: true,
logging: false,
entities: [Resource],
});
24 changes: 24 additions & 0 deletions src/problem5/dto/resource.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

export const StatusEnum = z.enum(['active', 'inactive']);

export const CreateDTO = z.object({
name: z.string().trim().min(1, 'name is required'),
description: z.string().optional(),
status: StatusEnum.optional(),
});

export const ModifyDTO = z.object({
name: z.string().trim().min(1, 'name must be a non-empty string').optional(),
description: z.string().optional(),
status: StatusEnum.optional(),
});

export const ResourceIdDTO = z.coerce.number().int().positive('id must be a positive integer');

export const ListDTO = z.object({
name: z.string().optional(),
status: StatusEnum.optional(),
page: z.coerce.number().int().min(1).default(1),
size: z.coerce.number().int().min(1).max(100).default(10),
});
1 change: 1 addition & 0 deletions src/problem5/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './resource.entity';
28 changes: 28 additions & 0 deletions src/problem5/entities/resource.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity()
export class Resource {
@PrimaryGeneratedColumn()
id!: number;

@Column()
name!: string;

@Column({ nullable: true })
description!: string;

@Column({ default: 'active' })
status!: string;

@CreateDateColumn()
createdAt!: Date;

@UpdateDateColumn()
updatedAt!: Date;
}
Loading