Skip to content

Commit

Permalink
Merge pull request #3 from acerbisgianluca/feature/get-available-prod…
Browse files Browse the repository at this point in the history
…ucts-and-categories

Add products and categories controllers
  • Loading branch information
acerbisgianluca authored Apr 3, 2024
2 parents fc31a7e + bd06fdf commit 4234f5e
Show file tree
Hide file tree
Showing 17 changed files with 342 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'plugin:drizzle/recommended'
],
plugins: ['@typescript-eslint'],
plugins: ['@typescript-eslint', 'drizzle'],
rules: {
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
},
Expand Down
156 changes: 156 additions & 0 deletions docs/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
openapi: 3.0.3
info:
title: FreshCart Market API
description: FreshCart Market API documentation
version: 1.0.0
servers:
- url: 'http://localhost:3000/api'
description: Local server
paths:
/products:
get:
summary: Get all available products
operationId: getAllAvailableProducts
description: Get all available products (quantity > 0).
tags:
- Products
parameters:
- $ref: '#/components/parameters/paginationOffset'
- $ref: '#/components/parameters/paginationLimit'
responses:
'200':
description: A list of products.
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/Product'
size:
type: integer
description: The number of products in the result set.
required:
- results
- size
'422':
$ref: '#/components/responses/ValidationError'
/categories:
get:
summary: Get all categories
operationId: getAllCategories
description: Get all categories with the number of products assigned to them.
tags:
- Categories
responses:
'200':
description: A list of categories.
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
allOf:
- $ref: '#/components/schemas/Category'
- type: object
properties:
numOfProducts:
type: integer
description: The number of products assigned to the category.
required:
- numOfProducts
size:
type: integer
description: The number of categories in the result set.
required:
- results
- size
components:
parameters:
paginationOffset:
in: query
name: offset
required: false
schema:
type: integer
minimum: 0
default: 0
description: The number of items to skip before starting to collect the result set.
paginationLimit:
in: query
name: limit
required: false
schema:
type: integer
minimum: 1
default: 25
description: The numbers of items to return.
schemas:
Category:
type: object
properties:
id:
type: integer
format: int64
description: The category ID.
readOnly: true
name:
type: string
description: The category name.
example: Fruits
required:
- id
- name
Product:
type: object
properties:
id:
type: integer
format: int64
description: The product ID.
readOnly: true
name:
type: string
description: The product name.
price:
type: number
format: float
description: The product price.
quantity:
type: integer
description: The product quantity.
category:
type: integer
description: The category ID.
imageUrl:
type: string
format: uri
description: The image url.
nullable: true
required:
- id
- name
- price
- quantity
- category
example:
id: 1
name: Apple
price: 1.99
quantity: 100
imageUrl: null
category: 2
responses:
ValidationError:
description: The parameters or the body of the request are invalid.
content:
text/plain:
schema:
type: string
example: 'Invalid parameters or request body.'
security: []
10 changes: 10 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from "drizzle-kit";

export default {
schema: "src/schema/*.ts",
out: "drizzle",
driver: 'better-sqlite',
dbCredentials: {
url: 'data/sqlite.db',
}
} satisfies Config;
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"build": "bun build src/index.ts",
"start": "NODE_ENV=production bun src/index.ts",
"test": "bun test",
"lint": "eslint src test --fix --ext .ts",
"lint": "eslint src --fix --ext .ts",
"studio": "drizzle-kit studio",
"push": "drizzle-kit push:sqlite"
"push": "drizzle-kit push:sqlite",
"fill-database": "bun scripts/seed.ts"
},
"dependencies": {
"drizzle-orm": "^0.30.6",
Expand All @@ -24,6 +25,7 @@
"drizzle-kit": "^0.20.14",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.5"
},
Expand Down
9 changes: 9 additions & 0 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {db} from "../src/libs/database.ts";
import {categories} from "../src/schema/categories.ts";

await db.insert(categories).values([
{name: 'Electronic'},
{name: 'Books'}
]);

console.log('Seeding complete.');
17 changes: 17 additions & 0 deletions src/controllers/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Elysia } from 'elysia';
import { setup } from '../setup.ts';
import { CategoryService } from '../services/category.ts';

export const categories = new Elysia({
name: 'Controller.Categories',
prefix: '/categories',
})
.use(setup)
.get('/', async () => {
const categories = await CategoryService.getAllCategoriesWithProductQuantity();

return {
results: categories,
size: categories.length,
};
});
24 changes: 24 additions & 0 deletions src/controllers/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Elysia } from 'elysia';
import { setup } from '../setup.ts';
import { ProductService } from '../services/product.ts';

export const products = new Elysia({
name: 'Controller.Products',
prefix: '/products',
})
.use(setup)
.get(
'/',
async ({ query: { offset, limit } }) => {
const availableProducts = await ProductService.getAllAvailableProducts({
offset,
limit,
});

return {
results: availableProducts,
size: availableProducts.length,
};
},
{ query: 'pagination.options' },
);
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Elysia } from 'elysia';
import { products } from './controllers/products.ts';
import { categories } from './controllers/category.ts';

const app = new Elysia({ prefix: '/api' }).use(products).use(categories).listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
16 changes: 16 additions & 0 deletions src/libs/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { Database } from 'bun:sqlite';
import type { SQLiteSelect } from 'drizzle-orm/sqlite-core';
import * as products from '../schema/products.ts';
import * as categories from '../schema/categories.ts';

const sqlite = new Database('data/sqlite.db');
export const db = drizzle(sqlite, { schema: { ...products, ...categories } });

export const withPagination = <T extends SQLiteSelect>(
query: T,
page: number,
pageSize: number,
) => {
return query.limit(pageSize).offset(page * pageSize);
};
3 changes: 3 additions & 0 deletions src/models/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { categories } from '../schema/categories.ts';

export type Category = typeof categories.$inferSelect;
18 changes: 18 additions & 0 deletions src/models/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Elysia, type Static, t } from 'elysia';

export const paginationOptions = t.Object({
offset: t.Optional(
t.Numeric({ minimum: 0, error: 'Must be a number greater than or equal to 0' }),
),
limit: t.Optional(
t.Numeric({ minimum: 1, error: 'Must be a number greater than or equal to 1' }),
),
});

export type PaginationOptions = Static<typeof paginationOptions>;

export const paginationModels = new Elysia({
name: 'Model.Pagination',
}).model({
'pagination.options': paginationOptions,
});
3 changes: 3 additions & 0 deletions src/models/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { products } from '../schema/products.ts';

export type Product = typeof products.$inferSelect;
12 changes: 12 additions & 0 deletions src/schema/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
import { products } from './products.ts';

export const categories = sqliteTable('categories', {
id: integer('id').primaryKey(),
name: text('name').notNull().unique(),
});

export const categoriesRelations = relations(categories, ({ many }) => ({
products: many(products),
}));
21 changes: 21 additions & 0 deletions src/schema/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { text, integer, sqliteTable, real } from 'drizzle-orm/sqlite-core';
import { categories } from './categories.ts';
import { relations } from 'drizzle-orm';

export const products = sqliteTable('products', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
imageUrl: text('image_url'),
price: real('price').notNull(),
quantity: integer('quantity').notNull(),
category: integer('category_id')
.references(() => categories.id)
.notNull(),
});

export const productsRelations = relations(products, ({ one }) => ({
category: one(categories, {
fields: [products.category],
references: [categories.id],
}),
}));
18 changes: 18 additions & 0 deletions src/services/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { db } from '../libs/database.ts';
import { categories } from '../schema/categories.ts';
import { count, eq } from 'drizzle-orm';
import { products } from '../schema/products.ts';

export abstract class CategoryService {
public static getAllCategoriesWithProductQuantity() {
return db
.select({
id: categories.id,
name: categories.name,
numOfProducts: count(products.id),
})
.from(categories)
.leftJoin(products, eq(categories.id, products.category))
.groupBy(categories.id, categories.name);
}
}
18 changes: 18 additions & 0 deletions src/services/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Product } from '../models/product.ts';
import { db } from '../libs/database.ts';
import type { PaginationOptions } from '../models/pagination.ts';

export abstract class ProductService {
public static getAllAvailableProducts(
paginationOptions: PaginationOptions,
): Promise<Product[]> {
const limit = paginationOptions.limit ?? 25;
const offset = paginationOptions.offset ?? 0;

return db.query.products.findMany({
where: (products, { gt }) => gt(products.quantity, 0),
limit,
offset,
});
}
}
4 changes: 4 additions & 0 deletions src/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Elysia } from 'elysia';
import { paginationModels } from './models/pagination.ts';

export const setup = new Elysia({ name: 'setup' }).use(paginationModels);

0 comments on commit 4234f5e

Please sign in to comment.