frontend-hamroun 1.1.90 → 1.2.0
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/dist/{src/backend → backend}/api-utils.d.ts +2 -2
- package/dist/backend/api-utils.js +135 -0
- package/dist/backend/auth.js +387 -0
- package/dist/{src/backend → backend}/database.d.ts +1 -1
- package/dist/backend/database.js +91 -0
- package/dist/{src/backend → backend}/model.d.ts +2 -2
- package/dist/backend/model.js +176 -0
- package/dist/{src/backend → backend}/router.d.ts +1 -1
- package/dist/backend/router.js +137 -0
- package/dist/backend/server.js +268 -0
- package/dist/batch.js +22 -0
- package/dist/cli/index.js +1 -0
- package/dist/{src/component.d.ts → component.d.ts} +1 -1
- package/dist/component.js +84 -0
- package/dist/components/Counter.js +2 -0
- package/dist/context.js +20 -0
- package/dist/frontend-hamroun.es.js +1680 -0
- package/dist/frontend-hamroun.es.js.map +1 -0
- package/dist/frontend-hamroun.umd.js +2 -0
- package/dist/frontend-hamroun.umd.js.map +1 -0
- package/dist/hooks.js +164 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +52 -355
- package/dist/jsx-runtime/index.d.ts +9 -0
- package/dist/jsx-runtime/index.js +16 -0
- package/dist/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/jsx-runtime/jsx-runtime.js +91 -0
- package/dist/{src/jsx-runtime.d.ts → jsx-runtime.d.ts} +1 -1
- package/dist/jsx-runtime.js +192 -0
- package/dist/renderer.js +51 -0
- package/dist/{src/server-renderer.d.ts → server-renderer.d.ts} +3 -0
- package/dist/server-renderer.js +102 -0
- package/dist/vdom.js +27 -0
- package/package.json +38 -52
- package/scripts/generate.js +134 -0
- package/src/backend/api-utils.ts +178 -0
- package/src/backend/auth.ts +543 -0
- package/src/backend/database.ts +104 -0
- package/src/backend/model.ts +196 -0
- package/src/backend/router.ts +176 -0
- package/src/backend/server.ts +330 -0
- package/src/backend/types.ts +257 -0
- package/src/batch.ts +24 -0
- package/src/cli/index.js +22 -40
- package/src/component.ts +98 -0
- package/src/components/Counter.tsx +4 -0
- package/src/context.ts +32 -0
- package/src/hooks.ts +211 -0
- package/src/index.ts +113 -0
- package/src/jsx-runtime/index.ts +24 -0
- package/src/jsx-runtime/jsx-dev-runtime.ts +0 -0
- package/src/jsx-runtime/jsx-runtime.ts +99 -0
- package/src/jsx-runtime.ts +226 -0
- package/src/renderer.ts +55 -0
- package/src/server-renderer.ts +114 -0
- package/src/types/bcrypt.d.ts +30 -0
- package/src/types/jsonwebtoken.d.ts +55 -0
- package/src/types.d.ts +26 -0
- package/src/types.ts +21 -0
- package/src/vdom.ts +34 -0
- package/templates/basic-app/package.json +17 -15
- package/templates/basic-app/postcss.config.js +1 -0
- package/templates/basic-app/src/App.tsx +65 -0
- package/templates/basic-app/src/api.ts +58 -0
- package/templates/basic-app/src/components/Counter.tsx +26 -0
- package/templates/basic-app/src/components/Header.tsx +9 -0
- package/templates/basic-app/src/components/TodoList.tsx +90 -0
- package/templates/basic-app/src/main.ts +20 -0
- package/templates/basic-app/src/server.ts +99 -0
- package/templates/basic-app/tailwind.config.js +23 -2
- package/bin/cli.js +0 -371
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -139269
- package/dist/index.mjs.map +0 -1
- package/dist/src/index.d.ts +0 -16
- package/dist/test/setupTests.d.ts +0 -4
- /package/dist/{src/backend → backend}/auth.d.ts +0 -0
- /package/dist/{src/backend → backend}/server.d.ts +0 -0
- /package/dist/{src/backend → backend}/types.d.ts +0 -0
- /package/dist/{test/backend.test.d.ts → backend/types.js} +0 -0
- /package/dist/{src/batch.d.ts → batch.d.ts} +0 -0
- /package/dist/{src/cli → cli}/index.d.ts +0 -0
- /package/dist/{src/components → components}/Counter.d.ts +0 -0
- /package/dist/{src/context.d.ts → context.d.ts} +0 -0
- /package/dist/{src/hooks.d.ts → hooks.d.ts} +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-dev-runtime.d.ts +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-runtime.d.ts +0 -0
- /package/dist/{src/renderer.d.ts → renderer.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{test/mockTest.d.ts → types.js} +0 -0
- /package/dist/{src/vdom.d.ts → vdom.d.ts} +0 -0
- /package/{dist/test/mongooseSetup.d.ts → src/cli/index.ts} +0 -0
@@ -0,0 +1,1680 @@
|
|
1
|
+
import express, { Router } from "express";
|
2
|
+
import { Router as Router2 } from "express";
|
3
|
+
import path from "path";
|
4
|
+
import compression from "compression";
|
5
|
+
import helmet from "helmet";
|
6
|
+
import morgan from "morgan";
|
7
|
+
import mongoose from "mongoose";
|
8
|
+
import fs from "fs";
|
9
|
+
import jwt from "jsonwebtoken";
|
10
|
+
import bcrypt from "bcrypt";
|
11
|
+
const globalObj = typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {};
|
12
|
+
function jsx(type, props, key) {
|
13
|
+
return {
|
14
|
+
type,
|
15
|
+
props: props || {},
|
16
|
+
key
|
17
|
+
};
|
18
|
+
}
|
19
|
+
function jsxs(type, props, key) {
|
20
|
+
return jsx(type, props, key);
|
21
|
+
}
|
22
|
+
function createElement$1(vnode) {
|
23
|
+
if (typeof vnode === "string" || typeof vnode === "number") {
|
24
|
+
return document.createTextNode(String(vnode));
|
25
|
+
}
|
26
|
+
if (typeof vnode.type === "function") {
|
27
|
+
const result = vnode.type(vnode.props);
|
28
|
+
return createElement$1(result);
|
29
|
+
}
|
30
|
+
const element = document.createElement(vnode.type);
|
31
|
+
Object.entries(vnode.props || {}).forEach(([name, value]) => {
|
32
|
+
if (name === "children") {
|
33
|
+
const children = Array.isArray(value) ? value : [value];
|
34
|
+
children.forEach((child) => {
|
35
|
+
if (child != null) {
|
36
|
+
const childElement = createElement$1(child);
|
37
|
+
if (process.env.NODE_ENV === "test" && typeof window !== "undefined") {
|
38
|
+
if (!globalObj.__renderStats) {
|
39
|
+
globalObj.__renderStats = {
|
40
|
+
elementsCreated: 0,
|
41
|
+
textNodesCreated: 0,
|
42
|
+
eventsAttached: 0,
|
43
|
+
renderTime: 0
|
44
|
+
};
|
45
|
+
if (typeof afterAll === "function") {
|
46
|
+
afterAll(() => {
|
47
|
+
try {
|
48
|
+
const fs2 = require("fs");
|
49
|
+
const path2 = require("path");
|
50
|
+
const statsPath = path2.resolve(process.cwd(), "jsx-runtime-stats.json");
|
51
|
+
fs2.writeFileSync(statsPath, JSON.stringify(globalObj.__renderStats, null, 2));
|
52
|
+
console.log(`JSX runtime stats written to ${statsPath}`);
|
53
|
+
} catch (error) {
|
54
|
+
console.error("Failed to write stats file:", error);
|
55
|
+
}
|
56
|
+
});
|
57
|
+
}
|
58
|
+
}
|
59
|
+
if (childElement instanceof Text) {
|
60
|
+
globalObj.__renderStats.textNodesCreated++;
|
61
|
+
} else {
|
62
|
+
globalObj.__renderStats.elementsCreated++;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
element.appendChild(childElement);
|
66
|
+
}
|
67
|
+
});
|
68
|
+
} else if (name.startsWith("on")) {
|
69
|
+
const eventName = name.toLowerCase().substring(2);
|
70
|
+
element.addEventListener(eventName, value);
|
71
|
+
if (process.env.NODE_ENV === "test" && typeof window !== "undefined" && globalObj.__renderStats) {
|
72
|
+
globalObj.__renderStats.eventsAttached++;
|
73
|
+
}
|
74
|
+
} else if (name === "className") {
|
75
|
+
element.setAttribute("class", value);
|
76
|
+
} else if (name === "style" && typeof value === "object") {
|
77
|
+
Object.entries(value).forEach(([styleProp, styleValue]) => {
|
78
|
+
element.style[styleProp] = String(styleValue);
|
79
|
+
});
|
80
|
+
} else {
|
81
|
+
element.setAttribute(name, value);
|
82
|
+
}
|
83
|
+
});
|
84
|
+
return element;
|
85
|
+
}
|
86
|
+
const Fragment = Symbol("Fragment");
|
87
|
+
if (typeof window !== "undefined") {
|
88
|
+
window.jsx = jsx;
|
89
|
+
window.jsxs = jsxs;
|
90
|
+
window.Fragment = Fragment;
|
91
|
+
}
|
92
|
+
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
93
|
+
async function createElement(vnode) {
|
94
|
+
var _a;
|
95
|
+
console.log("Creating element from:", vnode);
|
96
|
+
if (!isBrowser) {
|
97
|
+
if (vnode == null) {
|
98
|
+
return { nodeType: 3, textContent: "" };
|
99
|
+
}
|
100
|
+
if (typeof vnode === "boolean") {
|
101
|
+
return { nodeType: 3, textContent: "" };
|
102
|
+
}
|
103
|
+
if (typeof vnode === "number" || typeof vnode === "string") {
|
104
|
+
return { nodeType: 3, textContent: String(vnode) };
|
105
|
+
}
|
106
|
+
if (Array.isArray(vnode)) {
|
107
|
+
const fragment = { nodeType: 11, childNodes: [] };
|
108
|
+
for (const child of vnode) {
|
109
|
+
const node = await createElement(child);
|
110
|
+
fragment.childNodes.push(node);
|
111
|
+
}
|
112
|
+
return fragment;
|
113
|
+
}
|
114
|
+
if ("type" in vnode && vnode.props !== void 0) {
|
115
|
+
const { type, props } = vnode;
|
116
|
+
if (typeof type === "function") {
|
117
|
+
try {
|
118
|
+
const result = await type(props || {});
|
119
|
+
const node = await createElement(result);
|
120
|
+
return node;
|
121
|
+
} catch (error) {
|
122
|
+
console.error("Error rendering component:", error);
|
123
|
+
return { nodeType: 3, textContent: "" };
|
124
|
+
}
|
125
|
+
}
|
126
|
+
const element = {
|
127
|
+
nodeType: 1,
|
128
|
+
tagName: type,
|
129
|
+
attributes: {},
|
130
|
+
style: {},
|
131
|
+
childNodes: [],
|
132
|
+
setAttribute: function(key, value) {
|
133
|
+
this.attributes[key] = value;
|
134
|
+
},
|
135
|
+
appendChild: function(child) {
|
136
|
+
this.childNodes.push(child);
|
137
|
+
}
|
138
|
+
};
|
139
|
+
for (const [key, value] of Object.entries(props || {})) {
|
140
|
+
if (key === "children")
|
141
|
+
continue;
|
142
|
+
if (key.startsWith("on") && typeof value === "function") {
|
143
|
+
const eventName = key.toLowerCase().slice(2);
|
144
|
+
if (!element.__events) {
|
145
|
+
element.__events = {};
|
146
|
+
}
|
147
|
+
element.__events[eventName] = value;
|
148
|
+
} else if (key === "style" && typeof value === "object") {
|
149
|
+
Object.assign(element.style, value);
|
150
|
+
} else if (key === "className") {
|
151
|
+
element.setAttribute("class", String(value));
|
152
|
+
} else if (key !== "key" && key !== "ref") {
|
153
|
+
element.setAttribute(key, String(value));
|
154
|
+
}
|
155
|
+
}
|
156
|
+
const children = props == null ? void 0 : props.children;
|
157
|
+
if (children != null) {
|
158
|
+
const childArray = Array.isArray(children) ? children.flat() : [children];
|
159
|
+
for (const child of childArray) {
|
160
|
+
const childNode = await createElement(child);
|
161
|
+
element.appendChild(childNode);
|
162
|
+
}
|
163
|
+
}
|
164
|
+
return element;
|
165
|
+
}
|
166
|
+
return { nodeType: 3, textContent: String(vnode) };
|
167
|
+
}
|
168
|
+
if (vnode == null) {
|
169
|
+
return document.createTextNode("");
|
170
|
+
}
|
171
|
+
if (typeof vnode === "boolean") {
|
172
|
+
return document.createTextNode("");
|
173
|
+
}
|
174
|
+
if (typeof vnode === "number" || typeof vnode === "string") {
|
175
|
+
return document.createTextNode(String(vnode));
|
176
|
+
}
|
177
|
+
if (Array.isArray(vnode)) {
|
178
|
+
const fragment = document.createDocumentFragment();
|
179
|
+
for (const child of vnode) {
|
180
|
+
const node = await createElement(child);
|
181
|
+
fragment.appendChild(node);
|
182
|
+
}
|
183
|
+
return fragment;
|
184
|
+
}
|
185
|
+
if ("type" in vnode && vnode.props !== void 0) {
|
186
|
+
const { type, props } = vnode;
|
187
|
+
if (typeof type === "function") {
|
188
|
+
try {
|
189
|
+
const result = await type(props || {});
|
190
|
+
const node = await createElement(result);
|
191
|
+
if (node instanceof Element) {
|
192
|
+
node.setAttribute("data-component-id", type.name || type.toString());
|
193
|
+
}
|
194
|
+
return node;
|
195
|
+
} catch (error) {
|
196
|
+
console.error("Error rendering component:", error);
|
197
|
+
return document.createTextNode("");
|
198
|
+
}
|
199
|
+
}
|
200
|
+
const element = document.createElement(type);
|
201
|
+
for (const [key, value] of Object.entries(props || {})) {
|
202
|
+
if (key === "children")
|
203
|
+
continue;
|
204
|
+
if (key.startsWith("on") && typeof value === "function") {
|
205
|
+
const eventName = key.toLowerCase().slice(2);
|
206
|
+
const existingHandler = (_a = element.__events) == null ? void 0 : _a[eventName];
|
207
|
+
if (existingHandler) {
|
208
|
+
element.removeEventListener(eventName, existingHandler);
|
209
|
+
}
|
210
|
+
element.addEventListener(eventName, value);
|
211
|
+
if (!element.__events) {
|
212
|
+
element.__events = {};
|
213
|
+
}
|
214
|
+
element.__events[eventName] = value;
|
215
|
+
} else if (key === "style" && typeof value === "object") {
|
216
|
+
Object.assign(element.style, value);
|
217
|
+
} else if (key === "className") {
|
218
|
+
element.setAttribute("class", String(value));
|
219
|
+
} else if (key !== "key" && key !== "ref") {
|
220
|
+
element.setAttribute(key, String(value));
|
221
|
+
}
|
222
|
+
}
|
223
|
+
const children = props == null ? void 0 : props.children;
|
224
|
+
if (children != null) {
|
225
|
+
const childArray = Array.isArray(children) ? children.flat() : [children];
|
226
|
+
for (const child of childArray) {
|
227
|
+
const childNode = await createElement(child);
|
228
|
+
element.appendChild(childNode);
|
229
|
+
}
|
230
|
+
}
|
231
|
+
return element;
|
232
|
+
}
|
233
|
+
return document.createTextNode(String(vnode));
|
234
|
+
}
|
235
|
+
class Component {
|
236
|
+
constructor(props = {}) {
|
237
|
+
this.state = {};
|
238
|
+
this.element = null;
|
239
|
+
this._mounted = false;
|
240
|
+
this.props = props;
|
241
|
+
}
|
242
|
+
componentDidMount() {
|
243
|
+
}
|
244
|
+
async setState(newState) {
|
245
|
+
const prevState = { ...this.state };
|
246
|
+
this.state = { ...prevState, ...newState };
|
247
|
+
console.log(`${this.constructor.name} state updated:`, {
|
248
|
+
prev: prevState,
|
249
|
+
next: this.state
|
250
|
+
});
|
251
|
+
await Promise.resolve();
|
252
|
+
if (this._mounted) {
|
253
|
+
await this.update();
|
254
|
+
} else {
|
255
|
+
await this.update();
|
256
|
+
}
|
257
|
+
}
|
258
|
+
_replayEvents(oldElement, newElement) {
|
259
|
+
const oldEvents = oldElement.__events || {};
|
260
|
+
Object.entries(oldEvents).forEach(([event, handler]) => {
|
261
|
+
newElement.addEventListener(event, handler);
|
262
|
+
});
|
263
|
+
newElement.__events = oldEvents;
|
264
|
+
}
|
265
|
+
_deepCloneWithEvents(node) {
|
266
|
+
const clone = node.cloneNode(false);
|
267
|
+
const events = node.__events || {};
|
268
|
+
clone.__events = events;
|
269
|
+
Object.entries(events).forEach(([event, handler]) => {
|
270
|
+
clone.addEventListener(event, handler);
|
271
|
+
});
|
272
|
+
Array.from(node.childNodes).forEach((child) => {
|
273
|
+
if (child instanceof HTMLElement) {
|
274
|
+
clone.appendChild(this._deepCloneWithEvents(child));
|
275
|
+
} else {
|
276
|
+
clone.appendChild(child.cloneNode(true));
|
277
|
+
}
|
278
|
+
});
|
279
|
+
return clone;
|
280
|
+
}
|
281
|
+
async update() {
|
282
|
+
const vdom = this.render();
|
283
|
+
if (!vdom)
|
284
|
+
return document.createTextNode("");
|
285
|
+
const rendered = await createElement(vdom);
|
286
|
+
if (rendered instanceof HTMLElement) {
|
287
|
+
return this._updateElement(rendered);
|
288
|
+
}
|
289
|
+
const wrapper = document.createElement("div");
|
290
|
+
wrapper.appendChild(rendered);
|
291
|
+
return this._updateElement(wrapper);
|
292
|
+
}
|
293
|
+
async _updateElement(rendered) {
|
294
|
+
const newElement = this._deepCloneWithEvents(rendered);
|
295
|
+
newElement.__instance = this;
|
296
|
+
if (!this.element) {
|
297
|
+
this.element = newElement;
|
298
|
+
if (!this._mounted) {
|
299
|
+
this._mounted = true;
|
300
|
+
queueMicrotask(() => this.componentDidMount());
|
301
|
+
}
|
302
|
+
} else if (this.element.parentNode) {
|
303
|
+
this.element.parentNode.replaceChild(newElement, this.element);
|
304
|
+
this.element = newElement;
|
305
|
+
}
|
306
|
+
return this.element;
|
307
|
+
}
|
308
|
+
render() {
|
309
|
+
throw new Error("Component must implement render() method");
|
310
|
+
}
|
311
|
+
}
|
312
|
+
let isBatching = false;
|
313
|
+
const queue = [];
|
314
|
+
function batchUpdates(fn) {
|
315
|
+
if (isBatching) {
|
316
|
+
queue.push(fn);
|
317
|
+
return;
|
318
|
+
}
|
319
|
+
isBatching = true;
|
320
|
+
try {
|
321
|
+
fn();
|
322
|
+
while (queue.length > 0) {
|
323
|
+
const nextFn = queue.shift();
|
324
|
+
nextFn == null ? void 0 : nextFn();
|
325
|
+
}
|
326
|
+
} finally {
|
327
|
+
isBatching = false;
|
328
|
+
}
|
329
|
+
}
|
330
|
+
let currentRender = 0;
|
331
|
+
const states = /* @__PURE__ */ new Map();
|
332
|
+
const stateIndices = /* @__PURE__ */ new Map();
|
333
|
+
const effects = /* @__PURE__ */ new Map();
|
334
|
+
const memos = /* @__PURE__ */ new Map();
|
335
|
+
const refs = /* @__PURE__ */ new Map();
|
336
|
+
let globalRenderCallback = null;
|
337
|
+
let globalContainer = null;
|
338
|
+
let currentElement = null;
|
339
|
+
const isServer = typeof window === "undefined";
|
340
|
+
const serverStates = /* @__PURE__ */ new Map();
|
341
|
+
function setRenderCallback(callback, element, container) {
|
342
|
+
globalRenderCallback = callback;
|
343
|
+
globalContainer = container;
|
344
|
+
currentElement = element;
|
345
|
+
}
|
346
|
+
function prepareRender() {
|
347
|
+
currentRender++;
|
348
|
+
stateIndices.set(currentRender, 0);
|
349
|
+
return currentRender;
|
350
|
+
}
|
351
|
+
function finishRender() {
|
352
|
+
if (isServer) {
|
353
|
+
serverStates.delete(currentRender);
|
354
|
+
}
|
355
|
+
currentRender = 0;
|
356
|
+
}
|
357
|
+
function useState(initial) {
|
358
|
+
if (!currentRender) {
|
359
|
+
throw new Error("useState must be called within a render");
|
360
|
+
}
|
361
|
+
if (isServer) {
|
362
|
+
if (!serverStates.has(currentRender)) {
|
363
|
+
serverStates.set(currentRender, /* @__PURE__ */ new Map());
|
364
|
+
}
|
365
|
+
const componentState = serverStates.get(currentRender);
|
366
|
+
const index22 = stateIndices.get(currentRender) || 0;
|
367
|
+
if (!componentState.has(index22)) {
|
368
|
+
componentState.set(index22, initial);
|
369
|
+
}
|
370
|
+
const state2 = componentState.get(index22);
|
371
|
+
const setState2 = (newValue) => {
|
372
|
+
};
|
373
|
+
stateIndices.set(currentRender, index22 + 1);
|
374
|
+
return [state2, setState2];
|
375
|
+
}
|
376
|
+
if (!states.has(currentRender)) {
|
377
|
+
states.set(currentRender, []);
|
378
|
+
}
|
379
|
+
const componentStates = states.get(currentRender);
|
380
|
+
const index2 = stateIndices.get(currentRender);
|
381
|
+
if (index2 >= componentStates.length) {
|
382
|
+
componentStates.push(initial);
|
383
|
+
}
|
384
|
+
const state = componentStates[index2];
|
385
|
+
const setState = (newValue) => {
|
386
|
+
const nextValue = typeof newValue === "function" ? newValue(componentStates[index2]) : newValue;
|
387
|
+
if (componentStates[index2] === nextValue)
|
388
|
+
return;
|
389
|
+
componentStates[index2] = nextValue;
|
390
|
+
if (isBatching) {
|
391
|
+
batchUpdates(() => rerender(currentRender));
|
392
|
+
} else {
|
393
|
+
rerender(currentRender);
|
394
|
+
}
|
395
|
+
};
|
396
|
+
stateIndices.set(currentRender, index2 + 1);
|
397
|
+
return [state, setState];
|
398
|
+
}
|
399
|
+
function useEffect(callback, deps) {
|
400
|
+
if (!currentRender)
|
401
|
+
throw new Error("useEffect must be called within a render");
|
402
|
+
const effectIndex = stateIndices.get(currentRender);
|
403
|
+
if (!effects.has(currentRender)) {
|
404
|
+
effects.set(currentRender, []);
|
405
|
+
}
|
406
|
+
const componentEffects = effects.get(currentRender);
|
407
|
+
const prevEffect = componentEffects[effectIndex];
|
408
|
+
if (!prevEffect || !deps || !prevEffect.deps || deps.some((dep, i) => dep !== prevEffect.deps[i])) {
|
409
|
+
if (prevEffect == null ? void 0 : prevEffect.cleanup) {
|
410
|
+
prevEffect.cleanup();
|
411
|
+
}
|
412
|
+
queueMicrotask(() => {
|
413
|
+
const cleanup = callback() || void 0;
|
414
|
+
componentEffects[effectIndex] = { cleanup, deps };
|
415
|
+
});
|
416
|
+
}
|
417
|
+
stateIndices.set(currentRender, effectIndex + 1);
|
418
|
+
}
|
419
|
+
function useMemo(factory, deps) {
|
420
|
+
if (!currentRender)
|
421
|
+
throw new Error("useMemo must be called within a render");
|
422
|
+
const memoIndex = stateIndices.get(currentRender);
|
423
|
+
if (!memos.has(currentRender)) {
|
424
|
+
memos.set(currentRender, []);
|
425
|
+
}
|
426
|
+
const componentMemos = memos.get(currentRender);
|
427
|
+
const prevMemo = componentMemos[memoIndex];
|
428
|
+
if (!prevMemo || deps && deps.some((dep, i) => !Object.is(dep, prevMemo.deps[i]))) {
|
429
|
+
const value = factory();
|
430
|
+
componentMemos[memoIndex] = { value, deps };
|
431
|
+
stateIndices.set(currentRender, memoIndex + 1);
|
432
|
+
return value;
|
433
|
+
}
|
434
|
+
stateIndices.set(currentRender, memoIndex + 1);
|
435
|
+
return prevMemo.value;
|
436
|
+
}
|
437
|
+
function useRef(initial) {
|
438
|
+
if (!currentRender)
|
439
|
+
throw new Error("useRef must be called within a render");
|
440
|
+
const refIndex = stateIndices.get(currentRender);
|
441
|
+
if (!refs.has(currentRender)) {
|
442
|
+
refs.set(currentRender, []);
|
443
|
+
}
|
444
|
+
const componentRefs = refs.get(currentRender);
|
445
|
+
if (refIndex >= componentRefs.length) {
|
446
|
+
const ref2 = { current: initial };
|
447
|
+
componentRefs.push(ref2);
|
448
|
+
stateIndices.set(currentRender, refIndex + 1);
|
449
|
+
return ref2;
|
450
|
+
}
|
451
|
+
const ref = componentRefs[refIndex];
|
452
|
+
stateIndices.set(currentRender, refIndex + 1);
|
453
|
+
return ref;
|
454
|
+
}
|
455
|
+
async function rerender(rendererId) {
|
456
|
+
try {
|
457
|
+
const componentEffects = effects.get(rendererId);
|
458
|
+
if (componentEffects) {
|
459
|
+
componentEffects.forEach((effect) => {
|
460
|
+
if (effect.cleanup)
|
461
|
+
effect.cleanup();
|
462
|
+
});
|
463
|
+
effects.set(rendererId, []);
|
464
|
+
}
|
465
|
+
if (globalRenderCallback && globalContainer && currentElement) {
|
466
|
+
await globalRenderCallback(currentElement, globalContainer);
|
467
|
+
}
|
468
|
+
} catch (error) {
|
469
|
+
console.error("Error during rerender:", error);
|
470
|
+
}
|
471
|
+
}
|
472
|
+
function useErrorBoundary() {
|
473
|
+
const [error, setError] = useState(null);
|
474
|
+
return [error, () => setError(null)];
|
475
|
+
}
|
476
|
+
let isHydrating = false;
|
477
|
+
async function hydrate(element, container) {
|
478
|
+
isHydrating = true;
|
479
|
+
try {
|
480
|
+
await render(element, container);
|
481
|
+
} finally {
|
482
|
+
isHydrating = false;
|
483
|
+
}
|
484
|
+
}
|
485
|
+
async function render(element, container) {
|
486
|
+
console.log("Rendering to:", container.id || "unnamed-container");
|
487
|
+
batchUpdates(async () => {
|
488
|
+
const rendererId = prepareRender();
|
489
|
+
try {
|
490
|
+
setRenderCallback(render, element, container);
|
491
|
+
const domNode = await createElement(element);
|
492
|
+
if (!isHydrating) {
|
493
|
+
container.innerHTML = "";
|
494
|
+
}
|
495
|
+
if (isHydrating && container.firstChild) {
|
496
|
+
console.log("Hydrating existing DOM");
|
497
|
+
} else {
|
498
|
+
container.appendChild(domNode);
|
499
|
+
}
|
500
|
+
} finally {
|
501
|
+
finishRender();
|
502
|
+
}
|
503
|
+
});
|
504
|
+
}
|
505
|
+
async function renderToString(element) {
|
506
|
+
prepareRender();
|
507
|
+
setRenderCallback(() => {
|
508
|
+
}, element, null);
|
509
|
+
try {
|
510
|
+
if (element == null)
|
511
|
+
return "";
|
512
|
+
if (typeof element === "boolean")
|
513
|
+
return "";
|
514
|
+
if (typeof element === "number" || typeof element === "string") {
|
515
|
+
return escapeHtml(String(element));
|
516
|
+
}
|
517
|
+
if (Array.isArray(element)) {
|
518
|
+
const children = await Promise.all(element.map(renderToString));
|
519
|
+
return children.join("");
|
520
|
+
}
|
521
|
+
if ("type" in element && element.props !== void 0) {
|
522
|
+
const { type, props } = element;
|
523
|
+
if (typeof type === "function") {
|
524
|
+
try {
|
525
|
+
prepareRender();
|
526
|
+
const result = await type(props || {});
|
527
|
+
const html2 = await renderToString(result);
|
528
|
+
finishRender();
|
529
|
+
return html2;
|
530
|
+
} catch (error) {
|
531
|
+
console.error("Error rendering component:", error);
|
532
|
+
return "";
|
533
|
+
}
|
534
|
+
}
|
535
|
+
if (type === Symbol.for("react.fragment") || type.name === "Fragment") {
|
536
|
+
if (props.children) {
|
537
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
538
|
+
const renderedChildren = await Promise.all(children.map(renderToString));
|
539
|
+
return renderedChildren.join("");
|
540
|
+
}
|
541
|
+
return "";
|
542
|
+
}
|
543
|
+
let html = `<${type}`;
|
544
|
+
for (const [key, value] of Object.entries(props || {})) {
|
545
|
+
if (key === "children" || key === "key")
|
546
|
+
continue;
|
547
|
+
if (key === "className") {
|
548
|
+
html += ` class="${escapeHtml(String(value))}"`;
|
549
|
+
} else if (key === "style" && typeof value === "object") {
|
550
|
+
html += ` style="${stringifyStyle(value || {})}"`;
|
551
|
+
} else if (!key.startsWith("on")) {
|
552
|
+
html += ` ${key}="${escapeHtml(String(value))}"`;
|
553
|
+
}
|
554
|
+
}
|
555
|
+
const voidElements = /* @__PURE__ */ new Set([
|
556
|
+
"area",
|
557
|
+
"base",
|
558
|
+
"br",
|
559
|
+
"col",
|
560
|
+
"embed",
|
561
|
+
"hr",
|
562
|
+
"img",
|
563
|
+
"input",
|
564
|
+
"link",
|
565
|
+
"meta",
|
566
|
+
"param",
|
567
|
+
"source",
|
568
|
+
"track",
|
569
|
+
"wbr"
|
570
|
+
]);
|
571
|
+
if (voidElements.has(type)) {
|
572
|
+
return html + "/>";
|
573
|
+
}
|
574
|
+
html += ">";
|
575
|
+
if (props == null ? void 0 : props.children) {
|
576
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
577
|
+
for (const child of children) {
|
578
|
+
html += await renderToString(child);
|
579
|
+
}
|
580
|
+
}
|
581
|
+
return html + `</${type}>`;
|
582
|
+
}
|
583
|
+
return escapeHtml(String(element));
|
584
|
+
} finally {
|
585
|
+
finishRender();
|
586
|
+
}
|
587
|
+
}
|
588
|
+
function escapeHtml(str) {
|
589
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
590
|
+
}
|
591
|
+
function stringifyStyle(style) {
|
592
|
+
return Object.entries(style).map(([key, value]) => `${hyphenate(key)}:${value}`).join(";");
|
593
|
+
}
|
594
|
+
function hyphenate(str) {
|
595
|
+
return str.replace(/[A-Z]/g, (match) => "-" + match.toLowerCase());
|
596
|
+
}
|
597
|
+
class DatabaseConnector {
|
598
|
+
// Renamed from isConnected to _connected
|
599
|
+
constructor(options) {
|
600
|
+
this.connection = null;
|
601
|
+
this._connected = false;
|
602
|
+
this.options = {
|
603
|
+
retryAttempts: 3,
|
604
|
+
retryDelay: 1e3,
|
605
|
+
connectionTimeout: 1e4,
|
606
|
+
autoIndex: true,
|
607
|
+
...options
|
608
|
+
};
|
609
|
+
}
|
610
|
+
/**
|
611
|
+
* Connect to the database
|
612
|
+
*/
|
613
|
+
async connect() {
|
614
|
+
try {
|
615
|
+
if (this._connected && this.connection) {
|
616
|
+
return this.connection;
|
617
|
+
}
|
618
|
+
mongoose.set("strictQuery", true);
|
619
|
+
let attempts = 0;
|
620
|
+
while (attempts < (this.options.retryAttempts || 3)) {
|
621
|
+
try {
|
622
|
+
await mongoose.connect(this.options.uri, {
|
623
|
+
dbName: this.options.name,
|
624
|
+
connectTimeoutMS: this.options.connectionTimeout,
|
625
|
+
autoIndex: this.options.autoIndex,
|
626
|
+
...this.options.options
|
627
|
+
});
|
628
|
+
break;
|
629
|
+
} catch (error) {
|
630
|
+
attempts++;
|
631
|
+
if (attempts >= (this.options.retryAttempts || 3)) {
|
632
|
+
throw error;
|
633
|
+
}
|
634
|
+
console.log(`Connection attempt ${attempts} failed. Retrying in ${this.options.retryDelay}ms...`);
|
635
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.retryDelay));
|
636
|
+
}
|
637
|
+
}
|
638
|
+
this.connection = mongoose.connection;
|
639
|
+
this._connected = true;
|
640
|
+
console.log(`Connected to MongoDB at ${this.options.uri}/${this.options.name}`);
|
641
|
+
this.connection.on("error", (err) => {
|
642
|
+
console.error("MongoDB connection error:", err);
|
643
|
+
this._connected = false;
|
644
|
+
});
|
645
|
+
this.connection.on("disconnected", () => {
|
646
|
+
console.log("MongoDB disconnected");
|
647
|
+
this._connected = false;
|
648
|
+
});
|
649
|
+
return this.connection;
|
650
|
+
} catch (error) {
|
651
|
+
console.error("Failed to connect to MongoDB:", error);
|
652
|
+
throw error;
|
653
|
+
}
|
654
|
+
}
|
655
|
+
/**
|
656
|
+
* Disconnect from the database
|
657
|
+
*/
|
658
|
+
async disconnect() {
|
659
|
+
if (this.connection) {
|
660
|
+
await mongoose.disconnect();
|
661
|
+
this._connected = false;
|
662
|
+
this.connection = null;
|
663
|
+
console.log("Disconnected from MongoDB");
|
664
|
+
}
|
665
|
+
}
|
666
|
+
/**
|
667
|
+
* Check if connected to the database
|
668
|
+
*/
|
669
|
+
isConnected() {
|
670
|
+
return this._connected;
|
671
|
+
}
|
672
|
+
/**
|
673
|
+
* Get the mongoose connection
|
674
|
+
*/
|
675
|
+
getConnection() {
|
676
|
+
return this.connection;
|
677
|
+
}
|
678
|
+
}
|
679
|
+
function createServer(options = {}) {
|
680
|
+
const app = express();
|
681
|
+
const {
|
682
|
+
port = 3e3,
|
683
|
+
staticDir = "public",
|
684
|
+
enableCors = true,
|
685
|
+
apiPrefix = "/api",
|
686
|
+
ssrEnabled = true,
|
687
|
+
middlewares = [],
|
688
|
+
enableCompression = true,
|
689
|
+
enableHelmet = true,
|
690
|
+
logFormat = "dev",
|
691
|
+
trustProxy = false,
|
692
|
+
showErrorDetails = process.env.NODE_ENV !== "production"
|
693
|
+
} = options;
|
694
|
+
if (trustProxy) {
|
695
|
+
app.set("trust proxy", trustProxy);
|
696
|
+
}
|
697
|
+
app.use(express.json());
|
698
|
+
app.use(express.urlencoded({ extended: true }));
|
699
|
+
if (enableCompression) {
|
700
|
+
app.use(compression());
|
701
|
+
}
|
702
|
+
if (enableHelmet) {
|
703
|
+
app.use(helmet({
|
704
|
+
contentSecurityPolicy: options.disableCSP ? false : void 0
|
705
|
+
}));
|
706
|
+
}
|
707
|
+
if (logFormat) {
|
708
|
+
app.use(morgan(logFormat));
|
709
|
+
}
|
710
|
+
if (enableCors) {
|
711
|
+
app.use((req, res, next) => {
|
712
|
+
res.header("Access-Control-Allow-Origin", "*");
|
713
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
714
|
+
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
715
|
+
if (req.method === "OPTIONS") {
|
716
|
+
return res.sendStatus(200);
|
717
|
+
}
|
718
|
+
next();
|
719
|
+
});
|
720
|
+
}
|
721
|
+
middlewares.forEach((middleware) => app.use(middleware));
|
722
|
+
if (staticDir) {
|
723
|
+
const staticPath = path.resolve(process.cwd(), staticDir);
|
724
|
+
if (fs.existsSync(staticPath)) {
|
725
|
+
app.use(express.static(staticPath, {
|
726
|
+
maxAge: options.staticCacheAge || "1d",
|
727
|
+
etag: true
|
728
|
+
}));
|
729
|
+
console.log(`📂 Serving static files from: ${staticPath}`);
|
730
|
+
} else {
|
731
|
+
console.warn(`⚠️ Static directory not found: ${staticPath}`);
|
732
|
+
}
|
733
|
+
}
|
734
|
+
let dbConnector = null;
|
735
|
+
app.connectToDatabase = async (dbOptions) => {
|
736
|
+
try {
|
737
|
+
if (dbConnector && dbConnector.isConnected()) {
|
738
|
+
console.log("✅ Using existing database connection");
|
739
|
+
return dbConnector;
|
740
|
+
}
|
741
|
+
dbConnector = new DatabaseConnector(dbOptions);
|
742
|
+
await dbConnector.connect();
|
743
|
+
console.log("✅ Database connected successfully");
|
744
|
+
process.on("SIGTERM", async () => {
|
745
|
+
if (dbConnector && dbConnector.isConnected()) {
|
746
|
+
await dbConnector.disconnect();
|
747
|
+
console.log("Database connection closed");
|
748
|
+
}
|
749
|
+
});
|
750
|
+
return dbConnector;
|
751
|
+
} catch (error) {
|
752
|
+
console.error("❌ Failed to connect to database:", error);
|
753
|
+
throw error;
|
754
|
+
}
|
755
|
+
};
|
756
|
+
const routes = {};
|
757
|
+
const ssrRoutes = {};
|
758
|
+
app.registerApi = (routePath, router, routerOptions = {}) => {
|
759
|
+
try {
|
760
|
+
const { prefix = apiPrefix } = routerOptions;
|
761
|
+
const fullPath = path.posix.join(prefix, routePath).replace(/\\/g, "/");
|
762
|
+
app.use(fullPath, router);
|
763
|
+
routes[fullPath] = { router, options: routerOptions };
|
764
|
+
console.log(`🔌 API registered: ${fullPath}`);
|
765
|
+
} catch (error) {
|
766
|
+
console.error(`❌ Failed to register API at ${routePath}:`, error);
|
767
|
+
}
|
768
|
+
return app;
|
769
|
+
};
|
770
|
+
app.registerSSR = (routePath, component, options2 = {}) => {
|
771
|
+
if (!ssrEnabled) {
|
772
|
+
console.log(`⚠️ SSR disabled: skipping registration of ${routePath}`);
|
773
|
+
return app;
|
774
|
+
}
|
775
|
+
ssrRoutes[routePath] = { component, options: options2 };
|
776
|
+
app.get(routePath, async (req, res, next) => {
|
777
|
+
try {
|
778
|
+
if (req.query.nossr === "true") {
|
779
|
+
return next();
|
780
|
+
}
|
781
|
+
const props = {
|
782
|
+
req,
|
783
|
+
res,
|
784
|
+
params: req.params,
|
785
|
+
query: req.query,
|
786
|
+
user: req.user,
|
787
|
+
...options2.props
|
788
|
+
};
|
789
|
+
const html = await renderToString(component(props));
|
790
|
+
res.send(`
|
791
|
+
<!DOCTYPE html>
|
792
|
+
<html lang="${options2.lang || "en"}">
|
793
|
+
<head>
|
794
|
+
<meta charset="UTF-8">
|
795
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
796
|
+
<title>${options2.title || "Frontend Hamroun App"}</title>
|
797
|
+
${options2.meta ? options2.meta.map((meta) => `<meta ${Object.entries(meta).map(([k, v]) => `${k}="${v}"`).join(" ")}>`).join("\n ") : ""}
|
798
|
+
${options2.head || ""}
|
799
|
+
${options2.styles ? `<style>${options2.styles}</style>` : ""}
|
800
|
+
${options2.styleSheets ? options2.styleSheets.map((sheet) => `<link rel="stylesheet" href="${sheet}">`).join("\n ") : ""}
|
801
|
+
</head>
|
802
|
+
<body ${options2.bodyAttributes || ""}>
|
803
|
+
<div id="${options2.rootId || "root"}">${html}</div>
|
804
|
+
<script>
|
805
|
+
window.__INITIAL_DATA__ = ${JSON.stringify(options2.initialData || {})};
|
806
|
+
<\/script>
|
807
|
+
${options2.scripts ? options2.scripts.map((script) => `<script src="${script}"><\/script>`).join("\n ") : ""}
|
808
|
+
</body>
|
809
|
+
</html>
|
810
|
+
`);
|
811
|
+
} catch (error) {
|
812
|
+
console.error("SSR Error:", error);
|
813
|
+
if (options2.fallback) {
|
814
|
+
return next();
|
815
|
+
}
|
816
|
+
res.status(500).send("Server rendering error");
|
817
|
+
}
|
818
|
+
});
|
819
|
+
console.log(`🖥️ SSR registered: ${routePath}`);
|
820
|
+
return app;
|
821
|
+
};
|
822
|
+
app.use((err, req, res, next) => {
|
823
|
+
console.error("Server error:", err);
|
824
|
+
const statusCode = err.statusCode || err.status || 500;
|
825
|
+
const isApiRequest = req.path.startsWith(apiPrefix);
|
826
|
+
if (isApiRequest) {
|
827
|
+
res.status(statusCode).json({
|
828
|
+
success: false,
|
829
|
+
error: showErrorDetails ? err.message : "Internal Server Error",
|
830
|
+
stack: showErrorDetails ? err.stack : void 0
|
831
|
+
});
|
832
|
+
} else {
|
833
|
+
res.status(statusCode).send(`
|
834
|
+
<!DOCTYPE html>
|
835
|
+
<html>
|
836
|
+
<head>
|
837
|
+
<title>Error - ${statusCode}</title>
|
838
|
+
<style>
|
839
|
+
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
840
|
+
.error { background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; }
|
841
|
+
.stack { background: #f8f9fa; padding: 1rem; border-radius: 4px; overflow: auto; }
|
842
|
+
</style>
|
843
|
+
</head>
|
844
|
+
<body>
|
845
|
+
<h1>Error ${statusCode}</h1>
|
846
|
+
<div class="error">${showErrorDetails ? err.message : "Internal Server Error"}</div>
|
847
|
+
${showErrorDetails && err.stack ? `<pre class="stack">${err.stack}</pre>` : ""}
|
848
|
+
</body>
|
849
|
+
</html>
|
850
|
+
`);
|
851
|
+
}
|
852
|
+
});
|
853
|
+
app.use((req, res) => {
|
854
|
+
const isApiRequest = req.path.startsWith(apiPrefix);
|
855
|
+
if (isApiRequest) {
|
856
|
+
res.status(404).json({ success: false, error: "Not Found" });
|
857
|
+
} else {
|
858
|
+
res.status(404).send(`
|
859
|
+
<!DOCTYPE html>
|
860
|
+
<html>
|
861
|
+
<head>
|
862
|
+
<title>404 - Not Found</title>
|
863
|
+
<style>
|
864
|
+
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
865
|
+
</style>
|
866
|
+
</head>
|
867
|
+
<body>
|
868
|
+
<h1>404 - Not Found</h1>
|
869
|
+
<p>The requested resource was not found on this server.</p>
|
870
|
+
<p><a href="/">Return to homepage</a></p>
|
871
|
+
</body>
|
872
|
+
</html>
|
873
|
+
`);
|
874
|
+
}
|
875
|
+
});
|
876
|
+
app.start = (callback) => {
|
877
|
+
const server = app.listen(port, () => {
|
878
|
+
console.log(`
|
879
|
+
🚀 Frontend Hamroun server running at http://localhost:${port}
|
880
|
+
${Object.keys(routes).length > 0 ? `
|
881
|
+
📡 Registered API Routes:
|
882
|
+
${Object.keys(routes).map((route) => ` ${route}`).join("\n")}` : ""}
|
883
|
+
${Object.keys(ssrRoutes).length > 0 ? `
|
884
|
+
🖥️ Registered SSR Routes:
|
885
|
+
${Object.keys(ssrRoutes).map((route) => ` ${route}`).join("\n")}` : ""}
|
886
|
+
`);
|
887
|
+
if (callback)
|
888
|
+
callback();
|
889
|
+
});
|
890
|
+
const gracefulShutdown = async (signal) => {
|
891
|
+
console.log(`${signal} signal received: closing HTTP server and cleaning up`);
|
892
|
+
server.close(async () => {
|
893
|
+
console.log("HTTP server closed");
|
894
|
+
if (dbConnector && dbConnector.isConnected()) {
|
895
|
+
await dbConnector.disconnect();
|
896
|
+
console.log("Database connection closed");
|
897
|
+
}
|
898
|
+
process.exit(0);
|
899
|
+
});
|
900
|
+
setTimeout(() => {
|
901
|
+
console.error("Could not close connections in time, forcefully shutting down");
|
902
|
+
process.exit(1);
|
903
|
+
}, 1e4);
|
904
|
+
};
|
905
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
906
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
907
|
+
return server;
|
908
|
+
};
|
909
|
+
return app;
|
910
|
+
}
|
911
|
+
const defaultErrorHandler = (error, { res }) => {
|
912
|
+
console.error("API Error:", error);
|
913
|
+
const status = error.status || error.statusCode || 500;
|
914
|
+
const message = error.message || "Internal server error";
|
915
|
+
res.status(status).json({
|
916
|
+
success: false,
|
917
|
+
error: message,
|
918
|
+
stack: process.env.NODE_ENV !== "production" ? error.stack : void 0
|
919
|
+
});
|
920
|
+
};
|
921
|
+
function createModelRouter(model, options = {}) {
|
922
|
+
const router = express.Router();
|
923
|
+
const { middleware = [], errorHandler = defaultErrorHandler } = options;
|
924
|
+
middleware.forEach((mw) => router.use(mw));
|
925
|
+
const handleRoute = (handler) => async (req, res, next) => {
|
926
|
+
try {
|
927
|
+
const context = { req, res, next, params: req.params, query: req.query, body: req.body };
|
928
|
+
return await handler(context);
|
929
|
+
} catch (error) {
|
930
|
+
const context = { req, res, next, params: req.params, query: req.query, body: req.body };
|
931
|
+
return errorHandler(error, context);
|
932
|
+
}
|
933
|
+
};
|
934
|
+
router.get("/", handleRoute(async ({ req, res }) => {
|
935
|
+
const paginationOptions = req.pagination || { page: 1, limit: 10 };
|
936
|
+
const result = await model.getAll(paginationOptions);
|
937
|
+
res.json({
|
938
|
+
success: true,
|
939
|
+
...result
|
940
|
+
});
|
941
|
+
}));
|
942
|
+
router.get("/:id", handleRoute(async ({ params, res }) => {
|
943
|
+
const item = await model.getById(params.id);
|
944
|
+
if (!item) {
|
945
|
+
res.status(404).json({
|
946
|
+
success: false,
|
947
|
+
error: "Item not found"
|
948
|
+
});
|
949
|
+
return;
|
950
|
+
}
|
951
|
+
res.json({
|
952
|
+
success: true,
|
953
|
+
data: item
|
954
|
+
});
|
955
|
+
}));
|
956
|
+
router.post("/", handleRoute(async ({ body, res }) => {
|
957
|
+
const newItem = await model.create(body);
|
958
|
+
res.status(201).json({
|
959
|
+
success: true,
|
960
|
+
data: newItem,
|
961
|
+
message: "Item created successfully"
|
962
|
+
});
|
963
|
+
}));
|
964
|
+
router.put("/:id", handleRoute(async ({ params, body, res }) => {
|
965
|
+
const updatedItem = await model.update(params.id, body);
|
966
|
+
if (!updatedItem) {
|
967
|
+
res.status(404).json({
|
968
|
+
success: false,
|
969
|
+
error: "Item not found"
|
970
|
+
});
|
971
|
+
return;
|
972
|
+
}
|
973
|
+
res.json({
|
974
|
+
success: true,
|
975
|
+
data: updatedItem,
|
976
|
+
message: "Item updated successfully"
|
977
|
+
});
|
978
|
+
}));
|
979
|
+
router.delete("/:id", handleRoute(async ({ params, res }) => {
|
980
|
+
const success = await model.delete(params.id);
|
981
|
+
if (!success) {
|
982
|
+
res.status(404).json({
|
983
|
+
success: false,
|
984
|
+
error: "Item not found"
|
985
|
+
});
|
986
|
+
return;
|
987
|
+
}
|
988
|
+
res.json({
|
989
|
+
success: true,
|
990
|
+
message: "Item deleted successfully"
|
991
|
+
});
|
992
|
+
}));
|
993
|
+
return router;
|
994
|
+
}
|
995
|
+
function createApiRouter$1(routeConfig, options = {}) {
|
996
|
+
const router = express.Router();
|
997
|
+
const { middleware = [], errorHandler = defaultErrorHandler } = options;
|
998
|
+
middleware.forEach((mw) => router.use(mw));
|
999
|
+
const handleRoute = (handler) => async (req, res, next) => {
|
1000
|
+
try {
|
1001
|
+
const context = { req, res, next, params: req.params, query: req.query, body: req.body };
|
1002
|
+
if (res.headersSent) {
|
1003
|
+
return;
|
1004
|
+
}
|
1005
|
+
return await handler(context);
|
1006
|
+
} catch (error) {
|
1007
|
+
if (res.headersSent) {
|
1008
|
+
console.error("Error occurred after response was sent:", error);
|
1009
|
+
return;
|
1010
|
+
}
|
1011
|
+
const context = { req, res, next, params: req.params, query: req.query, body: req.body };
|
1012
|
+
return errorHandler(error, context);
|
1013
|
+
}
|
1014
|
+
};
|
1015
|
+
Object.entries(routeConfig).forEach(([path2, config]) => {
|
1016
|
+
const { method, handler } = config;
|
1017
|
+
router[method](path2, handleRoute(handler));
|
1018
|
+
});
|
1019
|
+
return router;
|
1020
|
+
}
|
1021
|
+
function apiResponse(success, data, message, error, meta) {
|
1022
|
+
const response = { success };
|
1023
|
+
if (data !== void 0)
|
1024
|
+
response.data = data;
|
1025
|
+
if (message)
|
1026
|
+
response.message = message;
|
1027
|
+
if (error)
|
1028
|
+
response.error = error;
|
1029
|
+
if (meta)
|
1030
|
+
response.meta = meta;
|
1031
|
+
return response;
|
1032
|
+
}
|
1033
|
+
function sendSuccess(res, data, message, statusCode = 200, meta) {
|
1034
|
+
res.status(statusCode).json(apiResponse(true, data, message, void 0, meta));
|
1035
|
+
}
|
1036
|
+
function sendError(res, error, statusCode = 400, meta) {
|
1037
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
1038
|
+
res.status(statusCode).json(apiResponse(false, void 0, void 0, errorMessage, meta));
|
1039
|
+
}
|
1040
|
+
function getPaginationParams(req) {
|
1041
|
+
const page = parseInt(req.query.page) || 1;
|
1042
|
+
const limit = parseInt(req.query.limit) || 10;
|
1043
|
+
const sort = req.query.sort || "createdAt";
|
1044
|
+
const order = req.query.order === "asc" ? "asc" : "desc";
|
1045
|
+
return { page, limit, sort, order };
|
1046
|
+
}
|
1047
|
+
function paginationMiddleware(req, res, next) {
|
1048
|
+
req.pagination = getPaginationParams(req);
|
1049
|
+
next();
|
1050
|
+
}
|
1051
|
+
function validateRequest(schema) {
|
1052
|
+
return (req, res, next) => {
|
1053
|
+
try {
|
1054
|
+
const { error, value } = schema.validate(req.body);
|
1055
|
+
if (error) {
|
1056
|
+
sendError(res, `Validation error: ${error.message}`, 400);
|
1057
|
+
return;
|
1058
|
+
}
|
1059
|
+
req.body = value;
|
1060
|
+
next();
|
1061
|
+
} catch (err) {
|
1062
|
+
sendError(res, "Validation error", 400);
|
1063
|
+
}
|
1064
|
+
};
|
1065
|
+
}
|
1066
|
+
function createApiRouter(options = {}, auth) {
|
1067
|
+
const router = Router();
|
1068
|
+
if (options.requireAuth && auth) {
|
1069
|
+
router.use(auth.authenticate);
|
1070
|
+
if (options.requiredRole) {
|
1071
|
+
router.use(auth.hasRole(options.requiredRole));
|
1072
|
+
}
|
1073
|
+
}
|
1074
|
+
if (options.rateLimit) {
|
1075
|
+
console.warn("Rate limiting is disabled: express-rate-limit dependency is not installed");
|
1076
|
+
}
|
1077
|
+
return router;
|
1078
|
+
}
|
1079
|
+
function asyncHandler(fn) {
|
1080
|
+
return (req, res, next) => {
|
1081
|
+
fn(req, res, next).catch(next);
|
1082
|
+
};
|
1083
|
+
}
|
1084
|
+
function createRestEndpoints(model) {
|
1085
|
+
const router = Router();
|
1086
|
+
router.get("/", paginationMiddleware, asyncHandler(async (req, res) => {
|
1087
|
+
const result = await model.getAll(req.pagination);
|
1088
|
+
sendSuccess(res, result);
|
1089
|
+
}));
|
1090
|
+
router.get("/:id", asyncHandler(async (req, res) => {
|
1091
|
+
const item = await model.getById(req.params.id);
|
1092
|
+
if (!item) {
|
1093
|
+
return sendError(res, "Item not found", 404);
|
1094
|
+
}
|
1095
|
+
sendSuccess(res, item);
|
1096
|
+
}));
|
1097
|
+
router.post("/", asyncHandler(async (req, res) => {
|
1098
|
+
const newItem = await model.create(req.body);
|
1099
|
+
sendSuccess(res, newItem, "Item created successfully", 201);
|
1100
|
+
}));
|
1101
|
+
router.put("/:id", asyncHandler(async (req, res) => {
|
1102
|
+
const updatedItem = await model.update(req.params.id, req.body);
|
1103
|
+
if (!updatedItem) {
|
1104
|
+
return sendError(res, "Item not found", 404);
|
1105
|
+
}
|
1106
|
+
sendSuccess(res, updatedItem, "Item updated successfully");
|
1107
|
+
}));
|
1108
|
+
router.delete("/:id", asyncHandler(async (req, res) => {
|
1109
|
+
const result = await model.delete(req.params.id);
|
1110
|
+
if (!result) {
|
1111
|
+
return sendError(res, "Item not found", 404);
|
1112
|
+
}
|
1113
|
+
sendSuccess(res, null, "Item deleted successfully");
|
1114
|
+
}));
|
1115
|
+
return router;
|
1116
|
+
}
|
1117
|
+
const crypto = {};
|
1118
|
+
class Auth {
|
1119
|
+
constructor(options) {
|
1120
|
+
this.loginAttempts = /* @__PURE__ */ new Map();
|
1121
|
+
this.login = async (req, res) => {
|
1122
|
+
try {
|
1123
|
+
const { username, password } = req.body;
|
1124
|
+
const ip = req.ip || req.connection.remoteAddress || "";
|
1125
|
+
if (!this.checkRateLimit(ip)) {
|
1126
|
+
res.status(429).json({
|
1127
|
+
success: false,
|
1128
|
+
message: "Too many login attempts. Please try again later."
|
1129
|
+
});
|
1130
|
+
return;
|
1131
|
+
}
|
1132
|
+
if (!username || !password) {
|
1133
|
+
res.status(400).json({
|
1134
|
+
success: false,
|
1135
|
+
message: "Username and password are required"
|
1136
|
+
});
|
1137
|
+
return;
|
1138
|
+
}
|
1139
|
+
if (!this.options.findUser) {
|
1140
|
+
res.status(500).json({
|
1141
|
+
success: false,
|
1142
|
+
message: "User finder function not configured"
|
1143
|
+
});
|
1144
|
+
return;
|
1145
|
+
}
|
1146
|
+
const user = await this.options.findUser(username);
|
1147
|
+
if (!user) {
|
1148
|
+
res.status(401).json({
|
1149
|
+
success: false,
|
1150
|
+
message: "Invalid credentials"
|
1151
|
+
});
|
1152
|
+
return;
|
1153
|
+
}
|
1154
|
+
const isPasswordValid = await this.options.verifyPassword(
|
1155
|
+
password,
|
1156
|
+
user.password
|
1157
|
+
);
|
1158
|
+
if (!isPasswordValid) {
|
1159
|
+
res.status(401).json({
|
1160
|
+
success: false,
|
1161
|
+
message: "Invalid credentials"
|
1162
|
+
});
|
1163
|
+
return;
|
1164
|
+
}
|
1165
|
+
const userWithoutPassword = { ...user };
|
1166
|
+
delete userWithoutPassword.password;
|
1167
|
+
const tokenPair = this.generateTokenPair({
|
1168
|
+
id: user.id || user._id,
|
1169
|
+
username: user.username,
|
1170
|
+
role: user.role || "user"
|
1171
|
+
});
|
1172
|
+
if (this.options.saveRefreshToken) {
|
1173
|
+
const refreshExpiry = /* @__PURE__ */ new Date();
|
1174
|
+
refreshExpiry.setSeconds(refreshExpiry.getSeconds() + this.getExpirationSeconds(this.options.refreshExpiration || "7d"));
|
1175
|
+
await this.options.saveRefreshToken(
|
1176
|
+
user.id || user._id,
|
1177
|
+
tokenPair.refreshToken,
|
1178
|
+
refreshExpiry
|
1179
|
+
);
|
1180
|
+
}
|
1181
|
+
if (req.body.useCookies) {
|
1182
|
+
this.setAuthCookies(res, tokenPair);
|
1183
|
+
}
|
1184
|
+
res.json({
|
1185
|
+
success: true,
|
1186
|
+
message: "Authentication successful",
|
1187
|
+
tokens: tokenPair,
|
1188
|
+
user: userWithoutPassword
|
1189
|
+
});
|
1190
|
+
} catch (error) {
|
1191
|
+
console.error("Authentication error:", error);
|
1192
|
+
res.status(500).json({
|
1193
|
+
success: false,
|
1194
|
+
message: "Authentication failed",
|
1195
|
+
error: process.env.NODE_ENV !== "production" ? error.message : void 0
|
1196
|
+
});
|
1197
|
+
}
|
1198
|
+
};
|
1199
|
+
this.refreshToken = async (req, res) => {
|
1200
|
+
var _a, _b;
|
1201
|
+
try {
|
1202
|
+
const refreshToken = ((_a = req.cookies) == null ? void 0 : _a.refreshToken) || req.body.refreshToken;
|
1203
|
+
if (!refreshToken) {
|
1204
|
+
res.status(401).json({
|
1205
|
+
success: false,
|
1206
|
+
message: "Refresh token required"
|
1207
|
+
});
|
1208
|
+
return;
|
1209
|
+
}
|
1210
|
+
const decoded = this.verifyToken(refreshToken, "refresh");
|
1211
|
+
if (this.options.verifyRefreshToken) {
|
1212
|
+
const isValid = await this.options.verifyRefreshToken(decoded.id, refreshToken);
|
1213
|
+
if (!isValid) {
|
1214
|
+
this.clearAuthCookies(res);
|
1215
|
+
res.status(401).json({
|
1216
|
+
success: false,
|
1217
|
+
message: "Invalid refresh token"
|
1218
|
+
});
|
1219
|
+
return;
|
1220
|
+
}
|
1221
|
+
}
|
1222
|
+
const tokenPair = this.generateTokenPair({
|
1223
|
+
id: decoded.id,
|
1224
|
+
// We need to fetch the user data again for complete payload
|
1225
|
+
...this.options.findUser ? await this.options.findUser(decoded.id) : {}
|
1226
|
+
});
|
1227
|
+
if (this.options.saveRefreshToken) {
|
1228
|
+
const refreshExpiry = /* @__PURE__ */ new Date();
|
1229
|
+
refreshExpiry.setSeconds(refreshExpiry.getSeconds() + this.getExpirationSeconds(this.options.refreshExpiration || "7d"));
|
1230
|
+
await this.options.saveRefreshToken(
|
1231
|
+
decoded.id,
|
1232
|
+
tokenPair.refreshToken,
|
1233
|
+
refreshExpiry
|
1234
|
+
);
|
1235
|
+
}
|
1236
|
+
const useCookies = ((_b = req.cookies) == null ? void 0 : _b.accessToken) || req.body.useCookies;
|
1237
|
+
if (useCookies) {
|
1238
|
+
this.setAuthCookies(res, tokenPair);
|
1239
|
+
}
|
1240
|
+
res.json({
|
1241
|
+
success: true,
|
1242
|
+
message: "Token refreshed successfully",
|
1243
|
+
tokens: tokenPair
|
1244
|
+
});
|
1245
|
+
} catch (error) {
|
1246
|
+
this.clearAuthCookies(res);
|
1247
|
+
res.status(401).json({
|
1248
|
+
success: false,
|
1249
|
+
message: "Invalid or expired refresh token",
|
1250
|
+
error: process.env.NODE_ENV !== "production" ? error.message : void 0
|
1251
|
+
});
|
1252
|
+
}
|
1253
|
+
};
|
1254
|
+
this.logout = async (req, res) => {
|
1255
|
+
var _a;
|
1256
|
+
try {
|
1257
|
+
this.clearAuthCookies(res);
|
1258
|
+
const refreshToken = ((_a = req.cookies) == null ? void 0 : _a.refreshToken) || req.body.refreshToken;
|
1259
|
+
if (refreshToken && this.options.saveRefreshToken) {
|
1260
|
+
try {
|
1261
|
+
const decoded = this.verifyToken(refreshToken, "refresh");
|
1262
|
+
if (typeof this.options.saveRefreshToken === "function") {
|
1263
|
+
await this.options.saveRefreshToken(decoded.id, "", /* @__PURE__ */ new Date());
|
1264
|
+
}
|
1265
|
+
} catch (e) {
|
1266
|
+
}
|
1267
|
+
}
|
1268
|
+
res.json({ success: true, message: "Logged out successfully" });
|
1269
|
+
} catch (error) {
|
1270
|
+
console.error("Logout error:", error);
|
1271
|
+
res.status(500).json({ success: false, message: "Logout failed", error: error.message });
|
1272
|
+
}
|
1273
|
+
};
|
1274
|
+
this.authenticate = (req, res, next) => {
|
1275
|
+
var _a;
|
1276
|
+
try {
|
1277
|
+
let token = (_a = req.cookies) == null ? void 0 : _a.accessToken;
|
1278
|
+
if (!token) {
|
1279
|
+
const authHeader = req.headers.authorization;
|
1280
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
1281
|
+
token = authHeader.split(" ")[1];
|
1282
|
+
}
|
1283
|
+
}
|
1284
|
+
if (!token) {
|
1285
|
+
res.status(401).json({
|
1286
|
+
success: false,
|
1287
|
+
message: "Authentication required"
|
1288
|
+
});
|
1289
|
+
return;
|
1290
|
+
}
|
1291
|
+
const decoded = this.verifyToken(token, "access");
|
1292
|
+
req.user = decoded;
|
1293
|
+
next();
|
1294
|
+
} catch (error) {
|
1295
|
+
res.status(401).json({
|
1296
|
+
success: false,
|
1297
|
+
message: "Invalid or expired token",
|
1298
|
+
error: process.env.NODE_ENV !== "production" ? error.message : void 0
|
1299
|
+
});
|
1300
|
+
}
|
1301
|
+
};
|
1302
|
+
this.hasRole = (role) => {
|
1303
|
+
return (req, res, next) => {
|
1304
|
+
const user = req.user;
|
1305
|
+
if (!user) {
|
1306
|
+
res.status(401).json({
|
1307
|
+
success: false,
|
1308
|
+
message: "Authentication required"
|
1309
|
+
});
|
1310
|
+
return;
|
1311
|
+
}
|
1312
|
+
const roles = Array.isArray(role) ? role : [role];
|
1313
|
+
if (roles.includes(user.role)) {
|
1314
|
+
next();
|
1315
|
+
} else {
|
1316
|
+
res.status(403).json({
|
1317
|
+
success: false,
|
1318
|
+
message: "Insufficient permissions"
|
1319
|
+
});
|
1320
|
+
}
|
1321
|
+
};
|
1322
|
+
};
|
1323
|
+
this.options = {
|
1324
|
+
tokenExpiration: "15m",
|
1325
|
+
refreshExpiration: "7d",
|
1326
|
+
saltRounds: 10,
|
1327
|
+
secureCookies: process.env.NODE_ENV === "production",
|
1328
|
+
httpOnlyCookies: true,
|
1329
|
+
rateLimit: true,
|
1330
|
+
...options,
|
1331
|
+
refreshSecret: options.refreshSecret || options.jwtSecret
|
1332
|
+
};
|
1333
|
+
if (!options.jwtSecret) {
|
1334
|
+
throw new Error("JWT secret is required for authentication");
|
1335
|
+
}
|
1336
|
+
if (!this.options.verifyPassword) {
|
1337
|
+
this.options.verifyPassword = this.verifyPasswordWithBcrypt;
|
1338
|
+
}
|
1339
|
+
}
|
1340
|
+
/**
|
1341
|
+
* Hash a password using bcrypt
|
1342
|
+
*/
|
1343
|
+
async hashPassword(password) {
|
1344
|
+
return await bcrypt.hash(password, this.options.saltRounds || 10);
|
1345
|
+
}
|
1346
|
+
/**
|
1347
|
+
* Verify a password against a hash using bcrypt
|
1348
|
+
*/
|
1349
|
+
async verifyPasswordWithBcrypt(password, hashedPassword) {
|
1350
|
+
return await bcrypt.compare(password, hashedPassword);
|
1351
|
+
}
|
1352
|
+
/**
|
1353
|
+
* Generate a cryptographically secure random token
|
1354
|
+
*/
|
1355
|
+
generateSecureToken(length = 32) {
|
1356
|
+
return crypto.randomBytes(length).toString("hex");
|
1357
|
+
}
|
1358
|
+
/**
|
1359
|
+
* Generate token pair (access token + refresh token)
|
1360
|
+
*/
|
1361
|
+
generateTokenPair(payload) {
|
1362
|
+
const expiresIn = this.getExpirationSeconds(this.options.tokenExpiration || "15m");
|
1363
|
+
const accessToken = jwt.sign(
|
1364
|
+
{ ...payload, type: "access" },
|
1365
|
+
this.options.jwtSecret,
|
1366
|
+
{ expiresIn: this.options.tokenExpiration }
|
1367
|
+
);
|
1368
|
+
const refreshToken = jwt.sign(
|
1369
|
+
{ id: payload.id, type: "refresh" },
|
1370
|
+
this.options.refreshSecret,
|
1371
|
+
{ expiresIn: this.options.refreshExpiration }
|
1372
|
+
);
|
1373
|
+
return {
|
1374
|
+
accessToken,
|
1375
|
+
refreshToken,
|
1376
|
+
expiresIn
|
1377
|
+
};
|
1378
|
+
}
|
1379
|
+
/**
|
1380
|
+
* Convert JWT expiration time to seconds
|
1381
|
+
*/
|
1382
|
+
getExpirationSeconds(expiration) {
|
1383
|
+
const unit = expiration.charAt(expiration.length - 1);
|
1384
|
+
const value = parseInt(expiration.slice(0, -1));
|
1385
|
+
switch (unit) {
|
1386
|
+
case "s":
|
1387
|
+
return value;
|
1388
|
+
case "m":
|
1389
|
+
return value * 60;
|
1390
|
+
case "h":
|
1391
|
+
return value * 60 * 60;
|
1392
|
+
case "d":
|
1393
|
+
return value * 60 * 60 * 24;
|
1394
|
+
default:
|
1395
|
+
return 3600;
|
1396
|
+
}
|
1397
|
+
}
|
1398
|
+
/**
|
1399
|
+
* Verify a JWT token
|
1400
|
+
*/
|
1401
|
+
verifyToken(token, type = "access") {
|
1402
|
+
try {
|
1403
|
+
const secret = type === "access" ? this.options.jwtSecret : this.options.refreshSecret;
|
1404
|
+
const decoded = jwt.verify(token, secret);
|
1405
|
+
if (typeof decoded === "object" && decoded.type !== type) {
|
1406
|
+
throw new Error("Invalid token type");
|
1407
|
+
}
|
1408
|
+
return decoded;
|
1409
|
+
} catch (error) {
|
1410
|
+
throw new Error("Invalid or expired token");
|
1411
|
+
}
|
1412
|
+
}
|
1413
|
+
/**
|
1414
|
+
* Set authentication cookies
|
1415
|
+
*/
|
1416
|
+
setAuthCookies(res, tokens) {
|
1417
|
+
res.cookie("accessToken", tokens.accessToken, {
|
1418
|
+
httpOnly: this.options.httpOnlyCookies,
|
1419
|
+
secure: this.options.secureCookies,
|
1420
|
+
domain: this.options.cookieDomain,
|
1421
|
+
sameSite: "strict",
|
1422
|
+
maxAge: tokens.expiresIn * 1e3
|
1423
|
+
});
|
1424
|
+
const refreshExpiresIn = this.getExpirationSeconds(this.options.refreshExpiration || "7d");
|
1425
|
+
res.cookie("refreshToken", tokens.refreshToken, {
|
1426
|
+
httpOnly: true,
|
1427
|
+
// Always HTTP only for refresh tokens
|
1428
|
+
secure: this.options.secureCookies,
|
1429
|
+
domain: this.options.cookieDomain,
|
1430
|
+
sameSite: "strict",
|
1431
|
+
maxAge: refreshExpiresIn * 1e3,
|
1432
|
+
path: "/api/auth/refresh"
|
1433
|
+
// Restrict to refresh endpoint
|
1434
|
+
});
|
1435
|
+
}
|
1436
|
+
/**
|
1437
|
+
* Clear authentication cookies
|
1438
|
+
*/
|
1439
|
+
clearAuthCookies(res) {
|
1440
|
+
res.clearCookie("accessToken");
|
1441
|
+
res.clearCookie("refreshToken", { path: "/api/auth/refresh" });
|
1442
|
+
}
|
1443
|
+
/**
|
1444
|
+
* Check and handle rate limiting
|
1445
|
+
*/
|
1446
|
+
checkRateLimit(ip) {
|
1447
|
+
if (!this.options.rateLimit) {
|
1448
|
+
return true;
|
1449
|
+
}
|
1450
|
+
const now = Date.now();
|
1451
|
+
const attempt = this.loginAttempts.get(ip);
|
1452
|
+
if (!attempt) {
|
1453
|
+
this.loginAttempts.set(ip, { count: 1, resetTime: now + 36e5 });
|
1454
|
+
return true;
|
1455
|
+
}
|
1456
|
+
if (now > attempt.resetTime) {
|
1457
|
+
this.loginAttempts.set(ip, { count: 1, resetTime: now + 36e5 });
|
1458
|
+
return true;
|
1459
|
+
}
|
1460
|
+
if (attempt.count >= 5) {
|
1461
|
+
return false;
|
1462
|
+
}
|
1463
|
+
attempt.count++;
|
1464
|
+
this.loginAttempts.set(ip, attempt);
|
1465
|
+
return true;
|
1466
|
+
}
|
1467
|
+
}
|
1468
|
+
function createAuth(options) {
|
1469
|
+
return new Auth(options);
|
1470
|
+
}
|
1471
|
+
function createModel(name, schema) {
|
1472
|
+
const mongooseModel = mongoose.model(name, schema);
|
1473
|
+
return {
|
1474
|
+
getAll: async (options) => {
|
1475
|
+
try {
|
1476
|
+
const { page = 1, limit = 10, sort = "_id", order = "desc" } = options || {};
|
1477
|
+
const skip = (page - 1) * limit;
|
1478
|
+
const sortOrder = order === "asc" ? 1 : -1;
|
1479
|
+
const sortOptions = { [sort]: sortOrder };
|
1480
|
+
const [results, total] = await Promise.all([
|
1481
|
+
mongooseModel.find().sort(sortOptions).skip(skip).limit(limit).exec(),
|
1482
|
+
mongooseModel.countDocuments().exec()
|
1483
|
+
]);
|
1484
|
+
const totalPages = Math.ceil(total / limit);
|
1485
|
+
return {
|
1486
|
+
data: results,
|
1487
|
+
pagination: {
|
1488
|
+
total,
|
1489
|
+
totalPages,
|
1490
|
+
currentPage: page,
|
1491
|
+
limit,
|
1492
|
+
hasNextPage: page < totalPages,
|
1493
|
+
hasPrevPage: page > 1
|
1494
|
+
}
|
1495
|
+
};
|
1496
|
+
} catch (error) {
|
1497
|
+
console.error(`Error in ${name}.getAll:`, error);
|
1498
|
+
throw new Error(`Failed to retrieve ${name} records: ${error.message}`);
|
1499
|
+
}
|
1500
|
+
},
|
1501
|
+
getById: async (id) => {
|
1502
|
+
try {
|
1503
|
+
if (!mongoose.isValidObjectId(id)) {
|
1504
|
+
return null;
|
1505
|
+
}
|
1506
|
+
return await mongooseModel.findById(id).exec();
|
1507
|
+
} catch (error) {
|
1508
|
+
console.error(`Error in ${name}.getById:`, error);
|
1509
|
+
throw new Error(`Failed to retrieve ${name} by ID: ${error.message}`);
|
1510
|
+
}
|
1511
|
+
},
|
1512
|
+
create: async (data) => {
|
1513
|
+
try {
|
1514
|
+
const newDocument = new mongooseModel(data);
|
1515
|
+
return await newDocument.save();
|
1516
|
+
} catch (error) {
|
1517
|
+
console.error(`Error in ${name}.create:`, error);
|
1518
|
+
throw new Error(`Failed to create ${name}: ${error.message}`);
|
1519
|
+
}
|
1520
|
+
},
|
1521
|
+
createMany: async (data) => {
|
1522
|
+
try {
|
1523
|
+
return await mongooseModel.insertMany(data);
|
1524
|
+
} catch (error) {
|
1525
|
+
console.error(`Error in ${name}.createMany:`, error);
|
1526
|
+
throw new Error(`Failed to create multiple ${name} records: ${error.message}`);
|
1527
|
+
}
|
1528
|
+
},
|
1529
|
+
update: async (id, data) => {
|
1530
|
+
try {
|
1531
|
+
if (!mongoose.isValidObjectId(id)) {
|
1532
|
+
return null;
|
1533
|
+
}
|
1534
|
+
return await mongooseModel.findByIdAndUpdate(
|
1535
|
+
id,
|
1536
|
+
{ $set: data },
|
1537
|
+
{ new: true, runValidators: true }
|
1538
|
+
).exec();
|
1539
|
+
} catch (error) {
|
1540
|
+
console.error(`Error in ${name}.update:`, error);
|
1541
|
+
throw new Error(`Failed to update ${name}: ${error.message}`);
|
1542
|
+
}
|
1543
|
+
},
|
1544
|
+
delete: async (id) => {
|
1545
|
+
try {
|
1546
|
+
if (!mongoose.isValidObjectId(id)) {
|
1547
|
+
return false;
|
1548
|
+
}
|
1549
|
+
const result = await mongooseModel.findByIdAndDelete(id).exec();
|
1550
|
+
return result !== null;
|
1551
|
+
} catch (error) {
|
1552
|
+
console.error(`Error in ${name}.delete:`, error);
|
1553
|
+
throw new Error(`Failed to delete ${name}: ${error.message}`);
|
1554
|
+
}
|
1555
|
+
},
|
1556
|
+
find: async (query, options) => {
|
1557
|
+
try {
|
1558
|
+
const { page = 1, limit = 10, sort = "_id", order = "desc" } = options || {};
|
1559
|
+
const skip = (page - 1) * limit;
|
1560
|
+
const sortOrder = order === "asc" ? 1 : -1;
|
1561
|
+
const sortOptions = { [sort]: sortOrder };
|
1562
|
+
const [results, total] = await Promise.all([
|
1563
|
+
mongooseModel.find(query).sort(sortOptions).skip(skip).limit(limit).exec(),
|
1564
|
+
mongooseModel.countDocuments(query).exec()
|
1565
|
+
]);
|
1566
|
+
const totalPages = Math.ceil(total / limit);
|
1567
|
+
return {
|
1568
|
+
data: results,
|
1569
|
+
pagination: {
|
1570
|
+
total,
|
1571
|
+
totalPages,
|
1572
|
+
currentPage: page,
|
1573
|
+
limit,
|
1574
|
+
hasNextPage: page < totalPages,
|
1575
|
+
hasPrevPage: page > 1
|
1576
|
+
}
|
1577
|
+
};
|
1578
|
+
} catch (error) {
|
1579
|
+
console.error(`Error in ${name}.find:`, error);
|
1580
|
+
throw new Error(`Failed to find ${name} records: ${error.message}`);
|
1581
|
+
}
|
1582
|
+
},
|
1583
|
+
count: async (query) => {
|
1584
|
+
try {
|
1585
|
+
return await mongooseModel.countDocuments(query || {}).exec();
|
1586
|
+
} catch (error) {
|
1587
|
+
console.error(`Error in ${name}.count:`, error);
|
1588
|
+
throw new Error(`Failed to count ${name} records: ${error.message}`);
|
1589
|
+
}
|
1590
|
+
},
|
1591
|
+
findOne: async (query) => {
|
1592
|
+
try {
|
1593
|
+
return await mongooseModel.findOne(query).exec();
|
1594
|
+
} catch (error) {
|
1595
|
+
console.error(`Error in ${name}.findOne:`, error);
|
1596
|
+
throw new Error(`Failed to find ${name} record: ${error.message}`);
|
1597
|
+
}
|
1598
|
+
}
|
1599
|
+
};
|
1600
|
+
}
|
1601
|
+
const FieldTypes = {
|
1602
|
+
String: { type: String },
|
1603
|
+
Number: { type: Number },
|
1604
|
+
Boolean: { type: Boolean },
|
1605
|
+
Date: { type: Date },
|
1606
|
+
ObjectId: { type: String },
|
1607
|
+
// Fallback to String instead of using Schema.Types.ObjectId
|
1608
|
+
// Helper functions for common field patterns
|
1609
|
+
Required: (fieldType) => ({ ...fieldType, required: true }),
|
1610
|
+
Unique: (fieldType) => ({ ...fieldType, unique: true }),
|
1611
|
+
Ref: (model) => ({
|
1612
|
+
type: String,
|
1613
|
+
// Fallback to String type for tests
|
1614
|
+
ref: model
|
1615
|
+
}),
|
1616
|
+
Enum: (values) => ({ type: String, enum: values }),
|
1617
|
+
Default: (fieldType, defaultValue) => ({ ...fieldType, default: defaultValue }),
|
1618
|
+
// Array field type
|
1619
|
+
Array: (fieldType) => ({ type: [fieldType] })
|
1620
|
+
};
|
1621
|
+
const index = {
|
1622
|
+
// Frontend
|
1623
|
+
jsx,
|
1624
|
+
jsxs,
|
1625
|
+
createElement: createElement$1,
|
1626
|
+
Fragment,
|
1627
|
+
Component,
|
1628
|
+
useState,
|
1629
|
+
useEffect,
|
1630
|
+
useRef,
|
1631
|
+
useMemo,
|
1632
|
+
useErrorBoundary,
|
1633
|
+
render,
|
1634
|
+
hydrate,
|
1635
|
+
renderToString,
|
1636
|
+
prepareRender,
|
1637
|
+
finishRender,
|
1638
|
+
batchUpdates,
|
1639
|
+
// Backend
|
1640
|
+
createServer,
|
1641
|
+
Router,
|
1642
|
+
createApiRouter
|
1643
|
+
};
|
1644
|
+
export {
|
1645
|
+
Component,
|
1646
|
+
DatabaseConnector,
|
1647
|
+
FieldTypes,
|
1648
|
+
Fragment,
|
1649
|
+
Router2 as Router,
|
1650
|
+
apiResponse,
|
1651
|
+
asyncHandler,
|
1652
|
+
batchUpdates,
|
1653
|
+
createApiRouter,
|
1654
|
+
createAuth,
|
1655
|
+
createApiRouter$1 as createCustomRouter,
|
1656
|
+
createElement$1 as createElement,
|
1657
|
+
createModel,
|
1658
|
+
createModelRouter,
|
1659
|
+
createRestEndpoints,
|
1660
|
+
createServer,
|
1661
|
+
index as default,
|
1662
|
+
finishRender,
|
1663
|
+
getPaginationParams,
|
1664
|
+
hydrate,
|
1665
|
+
jsx,
|
1666
|
+
jsxs,
|
1667
|
+
paginationMiddleware,
|
1668
|
+
prepareRender,
|
1669
|
+
render,
|
1670
|
+
renderToString,
|
1671
|
+
sendError,
|
1672
|
+
sendSuccess,
|
1673
|
+
useEffect,
|
1674
|
+
useErrorBoundary,
|
1675
|
+
useMemo,
|
1676
|
+
useRef,
|
1677
|
+
useState,
|
1678
|
+
validateRequest
|
1679
|
+
};
|
1680
|
+
//# sourceMappingURL=frontend-hamroun.es.js.map
|