auth init
This commit is contained in:
parent
3fd769a36f
commit
51bfb1513a
29 changed files with 1606 additions and 50 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"name": "backend",
|
||||
"module": "index.ts",
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -14,8 +15,10 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"@trpc/server": "^11.6.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"data-manager-sqlite": "*",
|
||||
"hono": "^4.12.18",
|
||||
"jose": "^6.2.3",
|
||||
"zod": "^3.25.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,51 @@
|
|||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors'
|
||||
import { env } from './lib/env-exporter'
|
||||
import { appRouter } from './trpc/router'
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { env } from "./lib/env-exporter";
|
||||
import { appRouter } from "./trpc/router";
|
||||
import { createContext } from "./trpc/context";
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
|
||||
const app = new Hono()
|
||||
const app = new Hono();
|
||||
|
||||
app.use(
|
||||
'*',
|
||||
"*",
|
||||
cors({
|
||||
origin: (origin) => {
|
||||
// Allow local dev UIs.
|
||||
if (origin === 'http://localhost:3000') return origin
|
||||
if (origin === 'http://localhost:3001') return origin
|
||||
// Non-browser clients (no Origin header)
|
||||
if (!origin) return '*'
|
||||
return null
|
||||
if (origin === "http://localhost:3000") return origin;
|
||||
if (origin === "http://localhost:3001") return origin;
|
||||
if (!origin) return "*";
|
||||
return null;
|
||||
},
|
||||
allowHeaders: ['content-type'],
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowHeaders: ["content-type"],
|
||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
||||
credentials: true,
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
app.get('/health', (c) => c.json({ ok: true }))
|
||||
app.get("/health", (c) => c.json({ ok: true }));
|
||||
|
||||
app.all('/trpc/*', (c) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: '/trpc',
|
||||
app.all("/trpc/*", async (c) => {
|
||||
const resHeaders = new Headers();
|
||||
const response = await fetchRequestHandler({
|
||||
endpoint: "/trpc",
|
||||
req: c.req.raw,
|
||||
router: appRouter,
|
||||
}),
|
||||
)
|
||||
createContext: () => createContext({ req: c.req.raw, resHeaders }),
|
||||
});
|
||||
|
||||
export { app }
|
||||
resHeaders.forEach((value, key) => {
|
||||
response.headers.set(key, value);
|
||||
});
|
||||
|
||||
const port = Number(env.PORT || 4004)
|
||||
return response;
|
||||
});
|
||||
|
||||
export { app };
|
||||
|
||||
const port = Number(env.PORT || 4004);
|
||||
Bun.serve({
|
||||
port,
|
||||
fetch: app.fetch,
|
||||
})
|
||||
});
|
||||
|
||||
console.log(`Backend listening on http://localhost:${port}`)
|
||||
console.log(`Backend listening on http://localhost:${port}`);
|
||||
|
|
|
|||
31
apps/backend/src/lib/auth.ts
Normal file
31
apps/backend/src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { SignJWT, jwtVerify } from "jose";
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || "pharmanager-dev-secret-change-me",
|
||||
);
|
||||
|
||||
const COOKIE_NAME = "pharmanager_token";
|
||||
const EXPIRY = "24h";
|
||||
|
||||
export interface JWTPayload {
|
||||
staffId: number;
|
||||
enterpriseId: number;
|
||||
}
|
||||
|
||||
export async function signJWT(payload: JWTPayload): Promise<string> {
|
||||
return new SignJWT({ ...payload })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime(EXPIRY)
|
||||
.sign(JWT_SECRET);
|
||||
}
|
||||
|
||||
export async function verifyJWT(token: string): Promise<JWTPayload | null> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
return payload as unknown as JWTPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { COOKIE_NAME };
|
||||
|
|
@ -5,12 +5,18 @@ import {
|
|||
createDrugInfoRepo,
|
||||
createUnitsRepo,
|
||||
createStockBatchesRepo,
|
||||
createEnterpriseRepo,
|
||||
createStaffRepo,
|
||||
createEnterpriseStaffRepo,
|
||||
type StorageSpacesRepo,
|
||||
type DistributorsRepo,
|
||||
type ProductsRepo,
|
||||
type DrugInfoRepo,
|
||||
type UnitsRepo,
|
||||
type StockBatchesRepo,
|
||||
type EnterpriseRepo,
|
||||
type StaffRepo,
|
||||
type EnterpriseStaffRepo,
|
||||
} from "data-manager-sqlite";
|
||||
|
||||
export class DataManager {
|
||||
|
|
@ -20,6 +26,9 @@ export class DataManager {
|
|||
readonly drugInfo: DrugInfoRepo;
|
||||
readonly units: UnitsRepo;
|
||||
readonly stockBatches: StockBatchesRepo;
|
||||
readonly enterprises: EnterpriseRepo;
|
||||
readonly staff: StaffRepo;
|
||||
readonly enterpriseStaff: EnterpriseStaffRepo;
|
||||
|
||||
constructor() {
|
||||
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
|
||||
|
|
@ -28,6 +37,9 @@ export class DataManager {
|
|||
const { repo: drugInfoRepo } = createDrugInfoRepo();
|
||||
const { repo: unitsRepo } = createUnitsRepo();
|
||||
const { repo: stockBatchesRepo } = createStockBatchesRepo();
|
||||
const { repo: enterpriseRepo } = createEnterpriseRepo();
|
||||
const { repo: staffRepo } = createStaffRepo();
|
||||
const { repo: enterpriseStaffRepo } = createEnterpriseStaffRepo();
|
||||
|
||||
this.storageSpaces = storageSpacesRepo;
|
||||
this.distributors = distributorsRepo;
|
||||
|
|
@ -35,5 +47,8 @@ export class DataManager {
|
|||
this.drugInfo = drugInfoRepo;
|
||||
this.units = unitsRepo;
|
||||
this.stockBatches = stockBatchesRepo;
|
||||
this.enterprises = enterpriseRepo;
|
||||
this.staff = staffRepo;
|
||||
this.enterpriseStaff = enterpriseStaffRepo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
apps/backend/src/trpc/context.ts
Normal file
26
apps/backend/src/trpc/context.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { COOKIE_NAME, verifyJWT, type JWTPayload } from "../lib/auth";
|
||||
|
||||
export interface Context {
|
||||
req: Request;
|
||||
resHeaders: Headers;
|
||||
staff?: JWTPayload;
|
||||
}
|
||||
|
||||
export async function createContext(opts: {
|
||||
req: Request;
|
||||
resHeaders: Headers;
|
||||
}): Promise<Context> {
|
||||
const cookieHeader = opts.req.headers.get("cookie") || "";
|
||||
const token = cookieHeader
|
||||
.split("; ")
|
||||
.find((c) => c.startsWith(`${COOKIE_NAME}=`))
|
||||
?.split("=")[1];
|
||||
|
||||
const staff = token ? (await verifyJWT(token)) ?? undefined : undefined;
|
||||
|
||||
return {
|
||||
req: opts.req,
|
||||
resHeaders: opts.resHeaders,
|
||||
staff,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,16 @@
|
|||
import { initTRPC } from "@trpc/server";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { Context } from "./context";
|
||||
|
||||
const t = initTRPC.context<Context>().create();
|
||||
|
||||
export const router = t.router;
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
|
||||
if (!ctx.staff) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({ ctx: ctx as Context & { staff: NonNullable<Context["staff"]> } });
|
||||
});
|
||||
|
||||
const t = initTRPC.create();
|
||||
export { t };
|
||||
|
|
|
|||
75
apps/backend/src/trpc/pharmanager/v1/auth.ts
Normal file
75
apps/backend/src/trpc/pharmanager/v1/auth.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { z } from "zod";
|
||||
import { COOKIE_NAME, signJWT } from "../../../lib/auth";
|
||||
import { dataManager } from "../../../lib/data-manager-instance";
|
||||
import { publicProcedure, protectedProcedure, router } from "../../init";
|
||||
|
||||
export const authRouter = router({
|
||||
login: publicProcedure
|
||||
.input(z.object({ username: z.string(), password: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const staff = await dataManager.staff.getStaffByUsername(
|
||||
input.username,
|
||||
);
|
||||
if (!staff) {
|
||||
throw new Error("Invalid username or password");
|
||||
}
|
||||
|
||||
const valid = await dataManager.staff.verifyPassword(
|
||||
staff,
|
||||
input.password,
|
||||
);
|
||||
if (!valid) {
|
||||
throw new Error("Invalid username or password");
|
||||
}
|
||||
|
||||
const enterpriseStaff = dataManager.enterpriseStaff.getByStaffId(
|
||||
staff.id,
|
||||
);
|
||||
if (!enterpriseStaff) {
|
||||
throw new Error("No enterprise assigned");
|
||||
}
|
||||
|
||||
const token = await signJWT({
|
||||
staffId: staff.id,
|
||||
enterpriseId: enterpriseStaff.enterprise_id,
|
||||
});
|
||||
|
||||
ctx.resHeaders.set(
|
||||
"Set-Cookie",
|
||||
`${COOKIE_NAME}=${token}; HttpOnly; Path=/; Max-Age=86400; SameSite=Lax`,
|
||||
);
|
||||
|
||||
return {
|
||||
staff: {
|
||||
id: staff.id,
|
||||
name: staff.name,
|
||||
username: staff.username,
|
||||
is_password_reset_needed: staff.is_password_reset_needed,
|
||||
},
|
||||
enterprise_id: enterpriseStaff.enterprise_id,
|
||||
};
|
||||
}),
|
||||
|
||||
whoAmI: protectedProcedure.query(async ({ ctx }) => {
|
||||
const staff = await dataManager.staff.getStaffById(ctx.staff.staffId);
|
||||
if (!staff) {
|
||||
throw new Error("Staff not found");
|
||||
}
|
||||
|
||||
const enterprise = await dataManager.enterprises.getEnterpriseById(
|
||||
ctx.staff.enterpriseId,
|
||||
);
|
||||
|
||||
return {
|
||||
staff: {
|
||||
id: staff.id,
|
||||
name: staff.name,
|
||||
username: staff.username,
|
||||
email: staff.email,
|
||||
mobile: staff.mobile,
|
||||
is_password_reset_needed: staff.is_password_reset_needed,
|
||||
},
|
||||
enterprise,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
import { t } from "./init";
|
||||
import { router } from "./init";
|
||||
import { storageRouter } from "./pharmanager/v1/storage";
|
||||
import { distributorRouter } from "./pharmanager/v1/distributor";
|
||||
import { productRouter } from "./pharmanager/v1/product";
|
||||
import { drugInfoRouter } from "./pharmanager/v1/drugInfo";
|
||||
import { unitsRouter } from "./pharmanager/v1/units";
|
||||
import { stockRouter } from "./pharmanager/v1/stock";
|
||||
import { authRouter } from "./pharmanager/v1/auth";
|
||||
|
||||
export const appRouter = t.router({
|
||||
export const appRouter = router({
|
||||
storage: storageRouter,
|
||||
distributor: distributorRouter,
|
||||
product: productRouter,
|
||||
drugInfo: drugInfoRouter,
|
||||
units: unitsRouter,
|
||||
stock: stockRouter,
|
||||
auth: authRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
CREATE TABLE `enterprises` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`owner_name` text NOT NULL,
|
||||
`mobile` text NOT NULL,
|
||||
`address` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `staff` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`email` text,
|
||||
`mobile` text,
|
||||
`added_on` text NOT NULL,
|
||||
`password` text NOT NULL,
|
||||
`is_password_reset_needed` integer DEFAULT true NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `staff_username_unique` ON `staff` (`username`);--> statement-breakpoint
|
||||
CREATE TABLE `roles` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint
|
||||
CREATE TABLE `permissions` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `permissions_name_unique` ON `permissions` (`name`);--> statement-breakpoint
|
||||
CREATE TABLE `role_permissions` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`role_id` integer NOT NULL,
|
||||
`permission_id` integer NOT NULL,
|
||||
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `staff_roles` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`staff_id` integer NOT NULL,
|
||||
`role_id` integer NOT NULL,
|
||||
FOREIGN KEY (`staff_id`) REFERENCES `staff`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `enterprise_staff` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`staff_id` integer NOT NULL,
|
||||
`enterprise_id` integer NOT NULL,
|
||||
FOREIGN KEY (`staff_id`) REFERENCES `staff`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`enterprise_id`) REFERENCES `enterprises`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `storage_spaces` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint
|
||||
CREATE INDEX `storage_spaces_enterprise_idx` ON `storage_spaces` (`enterprise_id`);--> statement-breakpoint
|
||||
ALTER TABLE `distributors` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint
|
||||
CREATE INDEX `distributors_enterprise_idx` ON `distributors` (`enterprise_id`);--> statement-breakpoint
|
||||
ALTER TABLE `products` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint
|
||||
CREATE INDEX `products_enterprise_idx` ON `products` (`enterprise_id`);--> statement-breakpoint
|
||||
ALTER TABLE `stock_batches` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint
|
||||
CREATE INDEX `stock_batches_enterprise_idx` ON `stock_batches` (`enterprise_id`);
|
||||
999
packages/data-manager-sqlite/drizzle/meta/0005_snapshot.json
Normal file
999
packages/data-manager-sqlite/drizzle/meta/0005_snapshot.json
Normal file
|
|
@ -0,0 +1,999 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "4ebc4f18-552b-459a-abef-dc2dfe600cd2",
|
||||
"prevId": "dfa18399-c3f0-4ef0-896b-632b38f67c1f",
|
||||
"tables": {
|
||||
"storage_spaces": {
|
||||
"name": "storage_spaces",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"aliases": {
|
||||
"name": "aliases",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"image_urls": {
|
||||
"name": "image_urls",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"enterprise_id": {
|
||||
"name": "enterprise_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"storage_spaces_enterprise_idx": {
|
||||
"name": "storage_spaces_enterprise_idx",
|
||||
"columns": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"storage_spaces_enterprise_id_enterprises_id_fk": {
|
||||
"name": "storage_spaces_enterprise_id_enterprises_id_fk",
|
||||
"tableFrom": "storage_spaces",
|
||||
"tableTo": "enterprises",
|
||||
"columnsFrom": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"distributors": {
|
||||
"name": "distributors",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"agency": {
|
||||
"name": "agency",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"contact": {
|
||||
"name": "contact",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mobile": {
|
||||
"name": "mobile",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enterprise_id": {
|
||||
"name": "enterprise_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"distributors_enterprise_idx": {
|
||||
"name": "distributors_enterprise_idx",
|
||||
"columns": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"distributors_enterprise_id_enterprises_id_fk": {
|
||||
"name": "distributors_enterprise_id_enterprises_id_fk",
|
||||
"tableFrom": "distributors",
|
||||
"tableTo": "enterprises",
|
||||
"columnsFrom": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"products": {
|
||||
"name": "products",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"brand": {
|
||||
"name": "brand",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"distributor_id": {
|
||||
"name": "distributor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"unit_id": {
|
||||
"name": "unit_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"procured_price": {
|
||||
"name": "procured_price",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mrp": {
|
||||
"name": "mrp",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"selling_price": {
|
||||
"name": "selling_price",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"reorder_level": {
|
||||
"name": "reorder_level",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"units_per_strip": {
|
||||
"name": "units_per_strip",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hide_product_from_public": {
|
||||
"name": "hide_product_from_public",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"hide_price_from_public": {
|
||||
"name": "hide_price_from_public",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"enterprise_id": {
|
||||
"name": "enterprise_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"products_enterprise_idx": {
|
||||
"name": "products_enterprise_idx",
|
||||
"columns": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"products_distributor_id_distributors_id_fk": {
|
||||
"name": "products_distributor_id_distributors_id_fk",
|
||||
"tableFrom": "products",
|
||||
"tableTo": "distributors",
|
||||
"columnsFrom": [
|
||||
"distributor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"products_unit_id_units_id_fk": {
|
||||
"name": "products_unit_id_units_id_fk",
|
||||
"tableFrom": "products",
|
||||
"tableTo": "units",
|
||||
"columnsFrom": [
|
||||
"unit_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"products_enterprise_id_enterprises_id_fk": {
|
||||
"name": "products_enterprise_id_enterprises_id_fk",
|
||||
"tableFrom": "products",
|
||||
"tableTo": "enterprises",
|
||||
"columnsFrom": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"drug_info": {
|
||||
"name": "drug_info",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"drug_info_name_unique": {
|
||||
"name": "drug_info_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"units": {
|
||||
"name": "units",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"units_name_unique": {
|
||||
"name": "units_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"product_compositions": {
|
||||
"name": "product_compositions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"drug_info_id": {
|
||||
"name": "drug_info_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"unit_id": {
|
||||
"name": "unit_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"product_compositions_product_id_products_id_fk": {
|
||||
"name": "product_compositions_product_id_products_id_fk",
|
||||
"tableFrom": "product_compositions",
|
||||
"tableTo": "products",
|
||||
"columnsFrom": [
|
||||
"product_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"product_compositions_drug_info_id_drug_info_id_fk": {
|
||||
"name": "product_compositions_drug_info_id_drug_info_id_fk",
|
||||
"tableFrom": "product_compositions",
|
||||
"tableTo": "drug_info",
|
||||
"columnsFrom": [
|
||||
"drug_info_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"product_compositions_unit_id_units_id_fk": {
|
||||
"name": "product_compositions_unit_id_units_id_fk",
|
||||
"tableFrom": "product_compositions",
|
||||
"tableTo": "units",
|
||||
"columnsFrom": [
|
||||
"unit_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"stock_batches": {
|
||||
"name": "stock_batches",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"arrived": {
|
||||
"name": "arrived",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"batch_no": {
|
||||
"name": "batch_no",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mfg": {
|
||||
"name": "mfg",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expiry": {
|
||||
"name": "expiry",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"rack_id": {
|
||||
"name": "rack_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"distributor_id": {
|
||||
"name": "distributor_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"enterprise_id": {
|
||||
"name": "enterprise_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"stock_batches_enterprise_idx": {
|
||||
"name": "stock_batches_enterprise_idx",
|
||||
"columns": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"stock_batches_product_id_products_id_fk": {
|
||||
"name": "stock_batches_product_id_products_id_fk",
|
||||
"tableFrom": "stock_batches",
|
||||
"tableTo": "products",
|
||||
"columnsFrom": [
|
||||
"product_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"stock_batches_rack_id_storage_spaces_id_fk": {
|
||||
"name": "stock_batches_rack_id_storage_spaces_id_fk",
|
||||
"tableFrom": "stock_batches",
|
||||
"tableTo": "storage_spaces",
|
||||
"columnsFrom": [
|
||||
"rack_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"stock_batches_distributor_id_distributors_id_fk": {
|
||||
"name": "stock_batches_distributor_id_distributors_id_fk",
|
||||
"tableFrom": "stock_batches",
|
||||
"tableTo": "distributors",
|
||||
"columnsFrom": [
|
||||
"distributor_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"stock_batches_enterprise_id_enterprises_id_fk": {
|
||||
"name": "stock_batches_enterprise_id_enterprises_id_fk",
|
||||
"tableFrom": "stock_batches",
|
||||
"tableTo": "enterprises",
|
||||
"columnsFrom": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"enterprises": {
|
||||
"name": "enterprises",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner_name": {
|
||||
"name": "owner_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mobile": {
|
||||
"name": "mobile",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"staff": {
|
||||
"name": "staff",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mobile": {
|
||||
"name": "mobile",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"added_on": {
|
||||
"name": "added_on",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_password_reset_needed": {
|
||||
"name": "is_password_reset_needed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"staff_username_unique": {
|
||||
"name": "staff_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"roles": {
|
||||
"name": "roles",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"roles_name_unique": {
|
||||
"name": "roles_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"permissions": {
|
||||
"name": "permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"permissions_name_unique": {
|
||||
"name": "permissions_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"role_permissions": {
|
||||
"name": "role_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permission_id": {
|
||||
"name": "permission_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"role_permissions_role_id_roles_id_fk": {
|
||||
"name": "role_permissions_role_id_roles_id_fk",
|
||||
"tableFrom": "role_permissions",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"role_permissions_permission_id_permissions_id_fk": {
|
||||
"name": "role_permissions_permission_id_permissions_id_fk",
|
||||
"tableFrom": "role_permissions",
|
||||
"tableTo": "permissions",
|
||||
"columnsFrom": [
|
||||
"permission_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"staff_roles": {
|
||||
"name": "staff_roles",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"staff_id": {
|
||||
"name": "staff_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"staff_roles_staff_id_staff_id_fk": {
|
||||
"name": "staff_roles_staff_id_staff_id_fk",
|
||||
"tableFrom": "staff_roles",
|
||||
"tableTo": "staff",
|
||||
"columnsFrom": [
|
||||
"staff_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"staff_roles_role_id_roles_id_fk": {
|
||||
"name": "staff_roles_role_id_roles_id_fk",
|
||||
"tableFrom": "staff_roles",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"enterprise_staff": {
|
||||
"name": "enterprise_staff",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"staff_id": {
|
||||
"name": "staff_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enterprise_id": {
|
||||
"name": "enterprise_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"enterprise_staff_staff_id_staff_id_fk": {
|
||||
"name": "enterprise_staff_staff_id_staff_id_fk",
|
||||
"tableFrom": "enterprise_staff",
|
||||
"tableTo": "staff",
|
||||
"columnsFrom": [
|
||||
"staff_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"enterprise_staff_enterprise_id_enterprises_id_fk": {
|
||||
"name": "enterprise_staff_enterprise_id_enterprises_id_fk",
|
||||
"tableFrom": "enterprise_staff",
|
||||
"tableTo": "enterprises",
|
||||
"columnsFrom": [
|
||||
"enterprise_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,13 @@
|
|||
"when": 1779533139096,
|
||||
"tag": "0004_ambiguous_captain_america",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1779546674621,
|
||||
"tag": "0005_yielding_silver_fox",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -11,12 +11,14 @@
|
|||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"drizzle-orm": "^0.44.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,56 @@
|
|||
import bcrypt from 'bcryptjs'
|
||||
import { createDb } from './db'
|
||||
import { runMigrations } from './migrate'
|
||||
|
||||
const { db, sqlite } = createDb()
|
||||
runMigrations(sqlite)
|
||||
|
||||
// Seed reference data
|
||||
// Seed reference data — units
|
||||
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('mg')")
|
||||
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('gm')")
|
||||
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('ml')")
|
||||
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('piece')")
|
||||
|
||||
// Seed enterprise
|
||||
sqlite.run("INSERT OR IGNORE INTO enterprises (id, name, type, owner_name, mobile) VALUES (1, 'Main Pharmacy', 'Pharmacy', 'Admin User', '0000000000')")
|
||||
|
||||
// Seed staff (admin user)
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const adminHash = bcrypt.hashSync('admin123', 10)
|
||||
sqlite.run(
|
||||
"INSERT OR IGNORE INTO staff (id, name, username, password, added_on, is_password_reset_needed) VALUES (1, 'Admin', 'admin', ?, ?, 1)",
|
||||
[adminHash, today],
|
||||
)
|
||||
|
||||
// Link staff to enterprise
|
||||
sqlite.run("INSERT OR IGNORE INTO enterprise_staff (id, staff_id, enterprise_id) VALUES (1, 1, 1)")
|
||||
|
||||
// Seed roles
|
||||
sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (1, 'Admin')")
|
||||
sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (2, 'Pharmacist')")
|
||||
sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (3, 'Manager')")
|
||||
sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (4, 'Cashier')")
|
||||
|
||||
// Seed permissions
|
||||
const permNames = [
|
||||
'product.read', 'product.write',
|
||||
'stock.read', 'stock.write',
|
||||
'distributor.read', 'distributor.write',
|
||||
'storage.read', 'storage.write',
|
||||
'billing.read', 'billing.write',
|
||||
'staff.read', 'staff.write',
|
||||
'customer.read', 'customer.write',
|
||||
]
|
||||
for (let i = 0; i < permNames.length; i++) {
|
||||
sqlite.run(`INSERT OR IGNORE INTO permissions (id, name) VALUES (${i + 1}, '${permNames[i]}')`)
|
||||
}
|
||||
|
||||
// Seed role_permissions — Admin gets all
|
||||
for (let i = 1; i <= permNames.length; i++) {
|
||||
sqlite.run(`INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (1, ${i})`)
|
||||
}
|
||||
|
||||
// Seed staff_roles — admin gets Admin role
|
||||
sqlite.run("INSERT OR IGNORE INTO staff_roles (staff_id, role_id) VALUES (1, 1)")
|
||||
|
||||
export { db, sqlite }
|
||||
|
|
|
|||
42
packages/data-manager-sqlite/src/enterpriseStaff.ts
Normal file
42
packages/data-manager-sqlite/src/enterpriseStaff.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import { db } from './db-instance'
|
||||
import { enterpriseStaff } from './schema/enterpriseStaff'
|
||||
|
||||
export type EnterpriseStaff = {
|
||||
id: number
|
||||
staff_id: number
|
||||
enterprise_id: number
|
||||
}
|
||||
|
||||
export type EnterpriseStaffRepo = {
|
||||
getByStaffId: (staffId: number) => EnterpriseStaff | null
|
||||
create: (input: { staff_id: number; enterprise_id: number }) => EnterpriseStaff
|
||||
}
|
||||
|
||||
function toEnterpriseStaff(row: typeof enterpriseStaff.$inferSelect): EnterpriseStaff {
|
||||
return {
|
||||
id: row.id,
|
||||
staff_id: row.staffId,
|
||||
enterprise_id: row.enterpriseId,
|
||||
}
|
||||
}
|
||||
|
||||
export function createEnterpriseStaffRepo(): { repo: EnterpriseStaffRepo } {
|
||||
const repo: EnterpriseStaffRepo = {
|
||||
getByStaffId(staffId) {
|
||||
const row = db.select().from(enterpriseStaff).where(eq(enterpriseStaff.staffId, staffId)).get()
|
||||
return row ? toEnterpriseStaff(row) : null
|
||||
},
|
||||
|
||||
create(input) {
|
||||
const created = db.insert(enterpriseStaff).values({
|
||||
staffId: input.staff_id,
|
||||
enterpriseId: input.enterprise_id,
|
||||
}).returning().get()
|
||||
return toEnterpriseStaff(created)
|
||||
},
|
||||
}
|
||||
|
||||
return { repo }
|
||||
}
|
||||
51
packages/data-manager-sqlite/src/enterprises.ts
Normal file
51
packages/data-manager-sqlite/src/enterprises.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import { db } from './db-instance'
|
||||
import { enterprises } from './schema/enterprises'
|
||||
|
||||
export type Enterprise = {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
owner_name: string
|
||||
mobile: string
|
||||
address: string | null
|
||||
}
|
||||
|
||||
export type EnterpriseRepo = {
|
||||
getEnterpriseById: (id: number) => Promise<Enterprise | null>
|
||||
createEnterprise: (input: { name: string; type: string; owner_name: string; mobile: string; address?: string | null }) => Promise<Enterprise>
|
||||
}
|
||||
|
||||
function toEnterprise(row: typeof enterprises.$inferSelect): Enterprise {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
owner_name: row.ownerName,
|
||||
mobile: row.mobile,
|
||||
address: row.address,
|
||||
}
|
||||
}
|
||||
|
||||
export function createEnterpriseRepo(): { repo: EnterpriseRepo } {
|
||||
const repo: EnterpriseRepo = {
|
||||
getEnterpriseById(id) {
|
||||
const row = db.select().from(enterprises).where(eq(enterprises.id, id)).get()
|
||||
return Promise.resolve(row ? toEnterprise(row) : null)
|
||||
},
|
||||
|
||||
createEnterprise(input) {
|
||||
const created = db.insert(enterprises).values({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
ownerName: input.owner_name,
|
||||
mobile: input.mobile,
|
||||
address: input.address ?? null,
|
||||
}).returning().get()
|
||||
return Promise.resolve(toEnterprise(created))
|
||||
},
|
||||
}
|
||||
|
||||
return { repo }
|
||||
}
|
||||
|
|
@ -40,3 +40,25 @@ export {
|
|||
type StockBatchesRepo,
|
||||
} from './stockBatches'
|
||||
export { stockBatches } from './schema/stockBatches'
|
||||
export { enterprises } from './schema/enterprises'
|
||||
export { staff } from './schema/staff'
|
||||
export { roles } from './schema/roles'
|
||||
export { permissions } from './schema/permissions'
|
||||
export { rolePermissions } from './schema/rolePermissions'
|
||||
export { staffRoles } from './schema/staffRoles'
|
||||
export { enterpriseStaff } from './schema/enterpriseStaff'
|
||||
export {
|
||||
createEnterpriseRepo,
|
||||
type Enterprise,
|
||||
type EnterpriseRepo,
|
||||
} from './enterprises'
|
||||
export {
|
||||
createStaffRepo,
|
||||
type Staff,
|
||||
type StaffRepo,
|
||||
} from './staff'
|
||||
export {
|
||||
createEnterpriseStaffRepo,
|
||||
type EnterpriseStaff,
|
||||
type EnterpriseStaffRepo,
|
||||
} from './enterpriseStaff'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||
import { enterprises } from './enterprises'
|
||||
|
||||
export const distributors = sqliteTable('distributors', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
|
|
@ -6,4 +7,7 @@ export const distributors = sqliteTable('distributors', {
|
|||
contact: text('contact').notNull(),
|
||||
mobile: text('mobile').notNull(),
|
||||
address: text('address'),
|
||||
})
|
||||
enterpriseId: integer('enterprise_id').references(() => enterprises.id),
|
||||
}, (table) => ({
|
||||
enterpriseIdx: index('distributors_enterprise_idx').on(table.enterpriseId),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
import { staff } from './staff'
|
||||
import { enterprises } from './enterprises'
|
||||
|
||||
export const enterpriseStaff = sqliteTable('enterprise_staff', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
staffId: integer('staff_id').notNull().references(() => staff.id),
|
||||
enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id),
|
||||
})
|
||||
10
packages/data-manager-sqlite/src/schema/enterprises.ts
Normal file
10
packages/data-manager-sqlite/src/schema/enterprises.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const enterprises = sqliteTable('enterprises', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
type: text('type').notNull(),
|
||||
ownerName: text('owner_name').notNull(),
|
||||
mobile: text('mobile').notNull(),
|
||||
address: text('address'),
|
||||
})
|
||||
|
|
@ -5,3 +5,10 @@ export * from './drugInfo'
|
|||
export * from './units'
|
||||
export * from './productCompositions'
|
||||
export * from './stockBatches'
|
||||
export * from './enterprises'
|
||||
export * from './staff'
|
||||
export * from './roles'
|
||||
export * from './permissions'
|
||||
export * from './rolePermissions'
|
||||
export * from './staffRoles'
|
||||
export * from './enterpriseStaff'
|
||||
|
|
|
|||
6
packages/data-manager-sqlite/src/schema/permissions.ts
Normal file
6
packages/data-manager-sqlite/src/schema/permissions.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const permissions = sqliteTable('permissions', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull().unique(),
|
||||
})
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable, text, real, index } from 'drizzle-orm/sqlite-core'
|
||||
import { distributors } from './distributors'
|
||||
import { units } from './units'
|
||||
import { enterprises } from './enterprises'
|
||||
|
||||
export const products = sqliteTable('products', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
|
|
@ -17,4 +18,7 @@ export const products = sqliteTable('products', {
|
|||
unitsPerStrip: integer('units_per_strip'),
|
||||
hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false),
|
||||
hidePriceFromPublic: integer('hide_price_from_public', { mode: 'boolean' }).notNull().default(false),
|
||||
})
|
||||
enterpriseId: integer('enterprise_id').references(() => enterprises.id),
|
||||
}, (table) => ({
|
||||
enterpriseIdx: index('products_enterprise_idx').on(table.enterpriseId),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
import { roles } from './roles'
|
||||
import { permissions } from './permissions'
|
||||
|
||||
export const rolePermissions = sqliteTable('role_permissions', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
roleId: integer('role_id').notNull().references(() => roles.id),
|
||||
permissionId: integer('permission_id').notNull().references(() => permissions.id),
|
||||
})
|
||||
6
packages/data-manager-sqlite/src/schema/roles.ts
Normal file
6
packages/data-manager-sqlite/src/schema/roles.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const roles = sqliteTable('roles', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull().unique(),
|
||||
})
|
||||
12
packages/data-manager-sqlite/src/schema/staff.ts
Normal file
12
packages/data-manager-sqlite/src/schema/staff.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const staff = sqliteTable('staff', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email'),
|
||||
mobile: text('mobile'),
|
||||
addedOn: text('added_on').notNull(),
|
||||
password: text('password').notNull(),
|
||||
isPasswordResetNeeded: integer('is_password_reset_needed', { mode: 'boolean' }).notNull().default(true),
|
||||
})
|
||||
9
packages/data-manager-sqlite/src/schema/staffRoles.ts
Normal file
9
packages/data-manager-sqlite/src/schema/staffRoles.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
import { staff } from './staff'
|
||||
import { roles } from './roles'
|
||||
|
||||
export const staffRoles = sqliteTable('staff_roles', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
staffId: integer('staff_id').notNull().references(() => staff.id),
|
||||
roleId: integer('role_id').notNull().references(() => roles.id),
|
||||
})
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||
import { products } from './products'
|
||||
import { storageSpaces } from './storageSpacesSchema'
|
||||
import { distributors } from './distributors'
|
||||
import { enterprises } from './enterprises'
|
||||
|
||||
export const stockBatches = sqliteTable('stock_batches', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
|
|
@ -14,4 +15,7 @@ export const stockBatches = sqliteTable('stock_batches', {
|
|||
distributorId: integer('distributor_id').references(() => distributors.id),
|
||||
quantity: integer('quantity').notNull().default(0),
|
||||
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
|
||||
})
|
||||
enterpriseId: integer('enterprise_id').references(() => enterprises.id),
|
||||
}, (table) => ({
|
||||
enterpriseIdx: index('stock_batches_enterprise_idx').on(table.enterpriseId),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||
import { enterprises } from './enterprises'
|
||||
|
||||
export const storageSpaces = sqliteTable('storage_spaces', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
aliases: text('aliases').notNull().default('[]'),
|
||||
// JSON array string of image URLs
|
||||
imageUrls: text('image_urls').notNull().default('[]'),
|
||||
})
|
||||
enterpriseId: integer('enterprise_id').references(() => enterprises.id),
|
||||
}, (table) => ({
|
||||
enterpriseIdx: index('storage_spaces_enterprise_idx').on(table.enterpriseId),
|
||||
}))
|
||||
|
|
|
|||
70
packages/data-manager-sqlite/src/staff.ts
Normal file
70
packages/data-manager-sqlite/src/staff.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
import { db } from './db-instance'
|
||||
import { staff } from './schema/staff'
|
||||
|
||||
export type Staff = {
|
||||
id: number
|
||||
name: string
|
||||
username: string
|
||||
email: string | null
|
||||
mobile: string | null
|
||||
added_on: string
|
||||
password: string
|
||||
is_password_reset_needed: boolean
|
||||
}
|
||||
|
||||
export type StaffRepo = {
|
||||
getStaffById: (id: number) => Promise<Staff | null>
|
||||
getStaffByUsername: (username: string) => Promise<Staff | null>
|
||||
createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean }) => Promise<Staff>
|
||||
verifyPassword: (staff: Staff, password: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
function toStaff(row: typeof staff.$inferSelect): Staff {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
email: row.email,
|
||||
mobile: row.mobile,
|
||||
added_on: row.addedOn,
|
||||
password: row.password,
|
||||
is_password_reset_needed: row.isPasswordResetNeeded,
|
||||
}
|
||||
}
|
||||
|
||||
export function createStaffRepo(): { repo: StaffRepo } {
|
||||
const repo: StaffRepo = {
|
||||
getStaffById(id) {
|
||||
const row = db.select().from(staff).where(eq(staff.id, id)).get()
|
||||
return Promise.resolve(row ? toStaff(row) : null)
|
||||
},
|
||||
|
||||
getStaffByUsername(username) {
|
||||
const row = db.select().from(staff).where(eq(staff.username, username)).get()
|
||||
return Promise.resolve(row ? toStaff(row) : null)
|
||||
},
|
||||
|
||||
createStaff(input) {
|
||||
const hashed = bcrypt.hashSync(input.password, 10)
|
||||
const created = db.insert(staff).values({
|
||||
name: input.name,
|
||||
username: input.username,
|
||||
email: input.email ?? null,
|
||||
mobile: input.mobile ?? null,
|
||||
addedOn: input.added_on,
|
||||
password: hashed,
|
||||
isPasswordResetNeeded: input.is_password_reset_needed ?? true,
|
||||
}).returning().get()
|
||||
return Promise.resolve(toStaff(created))
|
||||
},
|
||||
|
||||
async verifyPassword(s, password) {
|
||||
return bcrypt.compare(password, s.password)
|
||||
},
|
||||
}
|
||||
|
||||
return { repo }
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue