panel-free 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,5 @@
1
1
  # panel-free
2
2
 
3
- Reusable SQLite + Express + EJS web panel for student exam projects.
3
+ A free, simple web panel template for small projects. Built with Node.js, TypeScript, Express, EJS, and SQLite.
4
4
 
5
- ## Version 1.0.2 fixes
6
-
7
- - Home/data/reports/admin data are preserved during EJS rendering.
8
- - Consumer TypeScript projects receive the required Express and better-sqlite3 type packages.
9
- - Public `Db` declaration is compatible with common TypeScript module settings.
10
- - CLI fallback remains available through `exports.default`.
11
-
12
- Main exam usage:
13
-
14
- ```powershell
15
- npm install panel-free
16
- npx panel-free
17
- ```
18
-
19
- The CLI starts `main.ts` from the current directory.
5
+ Created by Alex Maksimov.
package/dist/main.md ADDED
@@ -0,0 +1,194 @@
1
+ import { num, panel, sqlite, type RequestBody } from "panel-free";
2
+ import { Service } from "./Service.js";
3
+
4
+ const db = sqlite("project.db");
5
+
6
+ db.exec(`
7
+ CREATE TABLE IF NOT EXISTS categories (
8
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9
+ name TEXT NOT NULL UNIQUE
10
+ );
11
+
12
+ CREATE TABLE IF NOT EXISTS users (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ name TEXT NOT NULL,
15
+ login TEXT NOT NULL UNIQUE,
16
+ password TEXT NOT NULL,
17
+ role TEXT NOT NULL CHECK (role IN ('user', 'manager', 'admin'))
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS records (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ title TEXT NOT NULL,
23
+ status TEXT NOT NULL,
24
+ category_id INTEGER NOT NULL,
25
+ user_id INTEGER NOT NULL,
26
+ FOREIGN KEY (category_id) REFERENCES categories(id),
27
+ FOREIGN KEY (user_id) REFERENCES users(id)
28
+ );
29
+ `);
30
+
31
+ const categoryCount = db.prepare("SELECT COUNT(*) AS total FROM categories").get() as { total: number };
32
+ if (categoryCount.total === 0) {
33
+ db.exec(`
34
+ INSERT INTO categories (name) VALUES ('Категория 1'), ('Категория 2');
35
+ `);
36
+ }
37
+
38
+ const userCount = db.prepare("SELECT COUNT(*) AS total FROM users").get() as { total: number };
39
+ if (userCount.total === 0) {
40
+ db.exec(`
41
+ INSERT INTO users (name, login, password, role) VALUES
42
+ ('Пользователь', 'user', 'user', 'user'),
43
+ ('Менеджер', 'manager', 'manager', 'manager'),
44
+ ('Администратор', 'admin', 'admin', 'admin');
45
+ `);
46
+ }
47
+
48
+ const recordCount = db.prepare("SELECT COUNT(*) AS total FROM records").get() as { total: number };
49
+ if (recordCount.total === 0) {
50
+ const userId = db.prepare("SELECT id FROM users WHERE login = 'user'").get() as { id: number };
51
+ const managerId = db.prepare("SELECT id FROM users WHERE login = 'manager'").get() as { id: number };
52
+ db.exec(`
53
+ INSERT INTO records (title, status, category_id, user_id) VALUES
54
+ ('Пример заявки пользователя', 'new', 1, ${userId.id}),
55
+ ('Пример заявки менеджера', 'done', 2, ${managerId.id});
56
+ `);
57
+ }
58
+
59
+ const service = new Service(db);
60
+ panel({
61
+ title: "Описание проекта",
62
+ port: 3000,
63
+ db,
64
+ logger: true,
65
+ labels: {
66
+ home: "Главная",
67
+ data: "Данные",
68
+ reports: "Аналитика",
69
+ reportsTitle: "Аналитические SQL-запросы",
70
+ empty: "Нет данных",
71
+ defaultSubmit: "Сохранить",
72
+ login: "Вход",
73
+ register: "Регистрация",
74
+ logout: "Выйти",
75
+ admin: "Админ-панель",
76
+ myRecords: "Мои заявки",
77
+ allRecords: "Все заявки"
78
+ },
79
+ auth: {
80
+ onLogin: (body: RequestBody) => service.login(String(body.login ?? ""), String(body.password ?? "")),
81
+ onRegister: (body: RequestBody) => service.register(
82
+ String(body.name ?? ""),
83
+ String(body.login ?? ""),
84
+ String(body.password ?? "")
85
+ )
86
+ },
87
+ records: {
88
+ listSql: (user) => user.role === "user"
89
+ ? `
90
+ SELECT r.id, r.title, r.status, c.name AS category
91
+ FROM records r
92
+ JOIN categories c ON c.id = r.category_id
93
+ WHERE r.user_id = ${user.id}
94
+ ORDER BY r.id DESC
95
+ `
96
+ : `
97
+ SELECT r.id, r.title, r.status, c.name AS category, u.name AS user
98
+ FROM records r
99
+ JOIN categories c ON c.id = r.category_id
100
+ JOIN users u ON u.id = r.user_id
101
+ ORDER BY r.id DESC
102
+ `,
103
+ form: {
104
+ title: "Добавить заявку",
105
+ action: "/create",
106
+ submit: "Добавить",
107
+ fields: {
108
+ title: { type: "text", label: "Название" },
109
+ categoryId: {
110
+ type: "select",
111
+ label: "Категория",
112
+ sql: "SELECT id, name FROM categories ORDER BY id"
113
+ },
114
+ userId: {
115
+ type: "select",
116
+ label: "Пользователь",
117
+ sql: "SELECT id, name FROM users ORDER BY id",
118
+ adminOnly: true
119
+ }
120
+ },
121
+ onSubmit: (body: RequestBody, user) => {
122
+ const ownerId = user.role === "admin" ? num(body.userId) : user.id;
123
+ service.create(String(body.title ?? ""), num(body.categoryId), ownerId);
124
+ }
125
+ },
126
+ admin: {
127
+ listSql: `
128
+ SELECT r.id,
129
+ r.title,
130
+ r.status,
131
+ r.category_id AS categoryId,
132
+ r.user_id AS userId
133
+ FROM records r
134
+ ORDER BY r.id DESC
135
+ `,
136
+ fields: {
137
+ title: { type: "text", label: "Название" },
138
+ status: {
139
+ type: "select",
140
+ label: "Статус",
141
+ sql: `
142
+ SELECT 'new' AS id, 'Новая' AS name
143
+ UNION ALL SELECT 'in_progress', 'В работе'
144
+ UNION ALL SELECT 'done', 'Завершена'
145
+ UNION ALL SELECT 'cancelled', 'Отменена'
146
+ `
147
+ },
148
+ categoryId: {
149
+ type: "select",
150
+ label: "Категория",
151
+ sql: "SELECT id, name FROM categories ORDER BY id"
152
+ },
153
+ userId: {
154
+ type: "select",
155
+ label: "Пользователь",
156
+ sql: "SELECT id, name FROM users ORDER BY id"
157
+ }
158
+ },
159
+ onUpdate: (id, body) => service.update(
160
+ id,
161
+ String(body.title ?? ""),
162
+ String(body.status ?? ""),
163
+ num(body.categoryId),
164
+ num(body.userId)
165
+ ),
166
+ onDelete: (id) => service.delete(id)
167
+ }
168
+ },
169
+ reports: {
170
+ "JOIN — связанные записи": `
171
+ SELECT r.title, c.name AS category, u.name AS user
172
+ FROM records r
173
+ JOIN categories c ON c.id = r.category_id
174
+ JOIN users u ON u.id = r.user_id
175
+ `,
176
+ "GROUP BY — количество по статусам": `
177
+ SELECT status, COUNT(*) AS total
178
+ FROM records
179
+ GROUP BY status
180
+ `,
181
+ "HAVING — статусы с количеством больше 1": `
182
+ SELECT status, COUNT(*) AS total
183
+ FROM records
184
+ GROUP BY status
185
+ HAVING COUNT(*) > 1
186
+ `,
187
+ "LEFT JOIN — категории без заявок": `
188
+ SELECT c.name AS category
189
+ FROM categories c
190
+ LEFT JOIN records r ON r.category_id = c.id
191
+ WHERE r.id IS NULL
192
+ `
193
+ }
194
+ });
@@ -0,0 +1,26 @@
1
+ ```mermaid
2
+ erDiagram
3
+ USERS ||--o{ RECORDS : creates
4
+ CATEGORIES ||--o{ RECORDS : classifies
5
+
6
+ USERS {
7
+ INTEGER id PK
8
+ TEXT name
9
+ TEXT login UK
10
+ TEXT password
11
+ TEXT role
12
+ }
13
+
14
+ CATEGORIES {
15
+ INTEGER id PK
16
+ TEXT name UK
17
+ }
18
+
19
+ RECORDS {
20
+ INTEGER id PK
21
+ TEXT title
22
+ TEXT status
23
+ INTEGER category_id FK
24
+ INTEGER user_id FK
25
+ }
26
+ ```
package/dist/ser.md ADDED
@@ -0,0 +1,85 @@
1
+ import { fail, type Db, type PanelUser, type RequestBody } from "panel-free";
2
+
3
+ type Row = { id: number };
4
+
5
+ export class Service {
6
+ constructor(private db: Db) {}
7
+
8
+ login(login: string, password: string): PanelUser | null {
9
+ const row = this.db.prepare(`
10
+ SELECT id, name, login, role
11
+ FROM users
12
+ WHERE login = ? AND password = ?
13
+ `).get(login.trim(), password) as PanelUser | undefined;
14
+ return row ?? null;
15
+ }
16
+
17
+ register(name: string, login: string, password: string): PanelUser {
18
+ const cleanName = name.trim();
19
+ const cleanLogin = login.trim();
20
+ if (!cleanName) fail("Имя обязательно");
21
+ if (!cleanLogin) fail("Логин обязателен");
22
+ if (!password) fail("Пароль обязателен");
23
+
24
+ const exists = this.db.prepare("SELECT id FROM users WHERE login = ?").get(cleanLogin) as Row | undefined;
25
+ if (exists) fail("Такой логин уже занят");
26
+
27
+ const result = this.db
28
+ .prepare("INSERT INTO users (name, login, password, role) VALUES (?, ?, ?, 'user')")
29
+ .run(cleanName, cleanLogin, password);
30
+
31
+ return {
32
+ id: Number(result.lastInsertRowid),
33
+ name: cleanName,
34
+ login: cleanLogin,
35
+ role: "user"
36
+ };
37
+ }
38
+
39
+ create(title: string, categoryId: number, userId: number) {
40
+ if (typeof title !== "string" || !title.trim()) {
41
+ fail("Название обязательно");
42
+ }
43
+ const category = this.db
44
+ .prepare("SELECT id FROM categories WHERE id = ?")
45
+ .get(categoryId) as Row | undefined;
46
+ if (!category) fail("Категория не найдена");
47
+
48
+ const user = this.db
49
+ .prepare("SELECT id FROM users WHERE id = ?")
50
+ .get(userId) as Row | undefined;
51
+ if (!user) fail("Пользователь не найден");
52
+
53
+ this.db
54
+ .prepare("INSERT INTO records (title, status, category_id, user_id) VALUES (?, 'new', ?, ?)")
55
+ .run(title.trim(), categoryId, userId);
56
+ }
57
+
58
+ createForUser(body: RequestBody, user: PanelUser) {
59
+ this.create(String(body.title ?? ""), Number(body.categoryId), user.id);
60
+ }
61
+
62
+ update(id: number, title: string, status: string, categoryId: number, userId: number) {
63
+ if (!Number.isInteger(id) || id <= 0) fail("Некорректный ID записи");
64
+ if (!title.trim()) fail("Название обязательно");
65
+ if (!['new', 'in_progress', 'done', 'cancelled'].includes(status)) fail("Некорректный статус");
66
+
67
+ const record = this.db.prepare("SELECT id FROM records WHERE id = ?").get(id) as Row | undefined;
68
+ if (!record) fail("Запись не найдена");
69
+
70
+ const category = this.db.prepare("SELECT id FROM categories WHERE id = ?").get(categoryId) as Row | undefined;
71
+ if (!category) fail("Категория не найдена");
72
+
73
+ const user = this.db.prepare("SELECT id FROM users WHERE id = ?").get(userId) as Row | undefined;
74
+ if (!user) fail("Пользователь не найден");
75
+
76
+ this.db
77
+ .prepare("UPDATE records SET title = ?, status = ?, category_id = ?, user_id = ? WHERE id = ?")
78
+ .run(title.trim(), status, categoryId, userId, id);
79
+ }
80
+
81
+ delete(id: number) {
82
+ if (!Number.isInteger(id) || id <= 0) fail("Некорректный ID записи");
83
+ this.db.prepare("DELETE FROM records WHERE id = ?").run(id);
84
+ }
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panel-free",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Small reusable SQLite + Express + EJS web panel for student projects.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",