phantomback 1.0.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.
@@ -0,0 +1,64 @@
1
+ import cors from 'cors';
2
+ import express from 'express';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ /**
6
+ * Create and configure the Express server with standard middleware
7
+ */
8
+ export function createExpressApp(config) {
9
+ const app = express();
10
+
11
+ // Body parsing
12
+ app.use(express.json({ limit: '10mb' }));
13
+ app.use(express.urlencoded({ extended: true }));
14
+
15
+ // CORS (allow everything in dev)
16
+ app.use(
17
+ cors({
18
+ origin: '*',
19
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
20
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
21
+ }),
22
+ );
23
+
24
+ // Request logger
25
+ app.use((req, _res, next) => {
26
+ logger.route(req.method, req.originalUrl);
27
+ next();
28
+ });
29
+
30
+ // Health check
31
+ app.get(`${config.prefix}/_health`, (_req, res) => {
32
+ res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
33
+ });
34
+
35
+ return app;
36
+ }
37
+
38
+ /**
39
+ * Add error handling middleware (call after all routes are registered)
40
+ */
41
+ export function addErrorHandling(app) {
42
+ // 404 handler
43
+ app.use((_req, res) => {
44
+ res.status(404).json({
45
+ success: false,
46
+ error: {
47
+ status: 404,
48
+ message: 'Route not found. Check your API prefix and resource names.',
49
+ },
50
+ });
51
+ });
52
+
53
+ // Global error handler
54
+ app.use((err, _req, res, _next) => {
55
+ logger.error(err.message);
56
+ res.status(err.status || 500).json({
57
+ success: false,
58
+ error: {
59
+ status: err.status || 500,
60
+ message: err.message || 'Internal Server Error',
61
+ },
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,274 @@
1
+ import express from 'express';
2
+ import { paginate } from '../features/pagination.js';
3
+ import { applyFilters } from '../features/filters.js';
4
+ import { applySort } from '../features/sorting.js';
5
+ import { applySearch } from '../features/search.js';
6
+ import { authMiddleware } from '../features/auth.js';
7
+ import { delayMiddleware } from '../features/delay.js';
8
+ import { validationMiddleware } from '../schema/validator.js';
9
+ import { detectRelations, getChildren } from '../data/relations.js';
10
+ import { asyncHandler, sendResponse, sendError } from '../utils/helpers.js';
11
+ import { logger } from '../utils/logger.js';
12
+
13
+ /**
14
+ * Generate REST CRUD routes for all resources
15
+ */
16
+ export function createRouter(config, store) {
17
+ const router = express.Router();
18
+ const { resources, prefix } = config;
19
+ const relations = detectRelations(resources);
20
+
21
+ // Apply global latency
22
+ if (config.latency) {
23
+ router.use(delayMiddleware(config.latency));
24
+ }
25
+
26
+ // Info endpoint: list all available routes
27
+ router.get(`${prefix}`, (_req, res) => {
28
+ const endpoints = {};
29
+ for (const name of Object.keys(resources)) {
30
+ endpoints[name] = {
31
+ list: `GET ${prefix}/${name}`,
32
+ getOne: `GET ${prefix}/${name}/:id`,
33
+ create: `POST ${prefix}/${name}`,
34
+ update: `PUT ${prefix}/${name}/:id`,
35
+ patch: `PATCH ${prefix}/${name}/:id`,
36
+ delete: `DELETE ${prefix}/${name}/:id`,
37
+ };
38
+
39
+ // Add nested routes info
40
+ const children = getChildren(name, relations);
41
+ if (children.length > 0) {
42
+ endpoints[name].nested = children.map(
43
+ (c) => `GET ${prefix}/${name}/:id/${c.childResource}`,
44
+ );
45
+ }
46
+ }
47
+
48
+ res.json({
49
+ success: true,
50
+ message: '👻 PhantomBack API is running!',
51
+ endpoints,
52
+ });
53
+ });
54
+
55
+ // Generate routes for each resource
56
+ for (const [resourceName, schema] of Object.entries(resources)) {
57
+ const fields = schema.fields || {};
58
+ const basePath = `${prefix}/${resourceName}`;
59
+ const resourceAuth = schema.auth || false;
60
+ const secret = config.auth?.secret;
61
+
62
+ // Optionally protect routes with auth
63
+ const protect = resourceAuth ? [authMiddleware(secret)] : [];
64
+
65
+ // Per-resource latency
66
+ if (schema.latency) {
67
+ router.use(basePath, delayMiddleware(schema.latency));
68
+ }
69
+
70
+ // ─── GET /resource ─── List all with pagination, filtering, sorting, search
71
+ router.get(
72
+ basePath,
73
+ ...protect,
74
+ asyncHandler(async (req, res) => {
75
+ let records = store.findAll(resourceName);
76
+
77
+ // Apply search
78
+ records = applySearch(records, req.query);
79
+
80
+ // Apply filters
81
+ records = applyFilters(records, req.query);
82
+
83
+ // Apply sort
84
+ records = applySort(records, req.query);
85
+
86
+ // Field selection
87
+ const selectParam = req.query.fields || req.query._fields || req.query.select;
88
+ if (selectParam) {
89
+ const selectedFields = selectParam.split(',').map((f) => f.trim());
90
+ records = records.map((record) => {
91
+ const picked = { id: record.id };
92
+ for (const f of selectedFields) {
93
+ if (f in record) picked[f] = record[f];
94
+ }
95
+ return picked;
96
+ });
97
+ }
98
+
99
+ // Apply pagination
100
+ const { data, meta } = paginate(records, req.query);
101
+
102
+ return sendResponse(res, 200, data, meta);
103
+ }),
104
+ );
105
+
106
+ // ─── GET /resource/:id ─── Get one by ID
107
+ router.get(
108
+ `${basePath}/:id`,
109
+ ...protect,
110
+ asyncHandler(async (req, res) => {
111
+ const record = store.findById(resourceName, req.params.id);
112
+ if (!record) {
113
+ return sendError(res, 404, `${resourceName} with id "${req.params.id}" not found`);
114
+ }
115
+ return sendResponse(res, 200, record);
116
+ }),
117
+ );
118
+
119
+ // ─── POST /resource ─── Create new record
120
+ router.post(
121
+ basePath,
122
+ ...protect,
123
+ validationMiddleware(fields, false),
124
+ asyncHandler(async (req, res) => {
125
+ // Unique field check
126
+ const uniqueError = checkUnique(resourceName, fields, req.validatedBody, store);
127
+ if (uniqueError) {
128
+ return sendError(res, 409, uniqueError);
129
+ }
130
+
131
+ const record = store.create(resourceName, req.validatedBody);
132
+ return sendResponse(res, 201, record);
133
+ }),
134
+ );
135
+
136
+ // ─── PUT /resource/:id ─── Full update
137
+ router.put(
138
+ `${basePath}/:id`,
139
+ ...protect,
140
+ validationMiddleware(fields, false),
141
+ asyncHandler(async (req, res) => {
142
+ const existing = store.findById(resourceName, req.params.id);
143
+ if (!existing) {
144
+ return sendError(res, 404, `${resourceName} with id "${req.params.id}" not found`);
145
+ }
146
+
147
+ // Unique check (exclude current record)
148
+ const uniqueError = checkUnique(
149
+ resourceName,
150
+ fields,
151
+ req.validatedBody,
152
+ store,
153
+ req.params.id,
154
+ );
155
+ if (uniqueError) {
156
+ return sendError(res, 409, uniqueError);
157
+ }
158
+
159
+ const updated = store.update(resourceName, req.params.id, req.validatedBody);
160
+ return sendResponse(res, 200, updated);
161
+ }),
162
+ );
163
+
164
+ // ─── PATCH /resource/:id ─── Partial update
165
+ router.patch(
166
+ `${basePath}/:id`,
167
+ ...protect,
168
+ validationMiddleware(fields, true),
169
+ asyncHandler(async (req, res) => {
170
+ const existing = store.findById(resourceName, req.params.id);
171
+ if (!existing) {
172
+ return sendError(res, 404, `${resourceName} with id "${req.params.id}" not found`);
173
+ }
174
+
175
+ const uniqueError = checkUnique(
176
+ resourceName,
177
+ fields,
178
+ req.validatedBody,
179
+ store,
180
+ req.params.id,
181
+ );
182
+ if (uniqueError) {
183
+ return sendError(res, 409, uniqueError);
184
+ }
185
+
186
+ const patched = store.patch(resourceName, req.params.id, req.validatedBody);
187
+ return sendResponse(res, 200, patched);
188
+ }),
189
+ );
190
+
191
+ // ─── DELETE /resource/:id ─── Delete
192
+ router.delete(
193
+ `${basePath}/:id`,
194
+ ...protect,
195
+ asyncHandler(async (req, res) => {
196
+ const existing = store.findById(resourceName, req.params.id);
197
+ if (!existing) {
198
+ return sendError(res, 404, `${resourceName} with id "${req.params.id}" not found`);
199
+ }
200
+
201
+ store.delete(resourceName, req.params.id);
202
+ return sendResponse(res, 200, {
203
+ message: `${resourceName} deleted successfully`,
204
+ id: req.params.id,
205
+ });
206
+ }),
207
+ );
208
+
209
+ // ─── Nested routes ─── GET /resource/:id/childResource
210
+ const children = getChildren(resourceName, relations);
211
+ for (const relation of children) {
212
+ const nestedPath = `${basePath}/:id/${relation.childResource}`;
213
+
214
+ router.get(
215
+ nestedPath,
216
+ ...protect,
217
+ asyncHandler(async (req, res) => {
218
+ // Verify parent exists
219
+ const parent = store.findById(resourceName, req.params.id);
220
+ if (!parent) {
221
+ return sendError(res, 404, `${resourceName} with id "${req.params.id}" not found`);
222
+ }
223
+
224
+ // Find child records where foreignKey === parent id
225
+ let records = store.findWhere(relation.childResource, (record) => {
226
+ return record[relation.foreignKey] === req.params.id;
227
+ });
228
+
229
+ records = applySearch(records, req.query);
230
+ records = applyFilters(records, req.query);
231
+ records = applySort(records, req.query);
232
+
233
+ const { data, meta } = paginate(records, req.query);
234
+ return sendResponse(res, 200, data, meta);
235
+ }),
236
+ );
237
+
238
+ logger.route('GET', nestedPath);
239
+ }
240
+
241
+ // Log registered routes
242
+ logger.route('GET', basePath);
243
+ logger.route('GET', `${basePath}/:id`);
244
+ logger.route('POST', basePath);
245
+ logger.route('PUT', `${basePath}/:id`);
246
+ logger.route('PATCH', `${basePath}/:id`);
247
+ logger.route('DELETE', `${basePath}/:id`);
248
+ }
249
+
250
+ return router;
251
+ }
252
+
253
+ /**
254
+ * Check unique constraints on fields
255
+ */
256
+ function checkUnique(resourceName, fields, data, store, excludeId = null) {
257
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
258
+ const def = typeof fieldDef === 'string' ? { type: fieldDef } : fieldDef;
259
+ if (!def.unique) continue;
260
+
261
+ const value = data[fieldName];
262
+ if (value === undefined) continue;
263
+
264
+ const existing = store.findAll(resourceName).find((r) => {
265
+ return r[fieldName] === value && r.id !== excludeId;
266
+ });
267
+
268
+ if (existing) {
269
+ return `A ${resourceName} with ${fieldName} "${value}" already exists`;
270
+ }
271
+ }
272
+
273
+ return null;
274
+ }
@@ -0,0 +1,114 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ /**
4
+ * Generate a unique ID
5
+ */
6
+ export function generateId() {
7
+ return randomUUID().split('-')[0];
8
+ }
9
+
10
+ /**
11
+ * Get current ISO timestamp
12
+ */
13
+ export function timestamp() {
14
+ return new Date().toISOString();
15
+ }
16
+
17
+ /**
18
+ * Deep clone an object (structured clone)
19
+ */
20
+ export function deepClone(obj) {
21
+ return structuredClone(obj);
22
+ }
23
+
24
+ /**
25
+ * Pick specific keys from an object
26
+ */
27
+ export function pick(obj, keys) {
28
+ const result = {};
29
+ for (const key of keys) {
30
+ if (key in obj) result[key] = obj[key];
31
+ }
32
+ return result;
33
+ }
34
+
35
+ /**
36
+ * Omit specific keys from an object
37
+ */
38
+ export function omit(obj, keys) {
39
+ const result = { ...obj };
40
+ for (const key of keys) {
41
+ delete result[key];
42
+ }
43
+ return result;
44
+ }
45
+
46
+ /**
47
+ * Pluralize a resource name (simple)
48
+ */
49
+ export function pluralize(str) {
50
+ if (str.endsWith('s')) return str;
51
+ if (str.endsWith('y')) return str.slice(0, -1) + 'ies';
52
+ return str + 's';
53
+ }
54
+
55
+ /**
56
+ * Singularize a resource name (simple)
57
+ */
58
+ export function singularize(str) {
59
+ if (str.endsWith('ies')) return str.slice(0, -3) + 'y';
60
+ if (str.endsWith('s')) return str.slice(0, -1);
61
+ return str;
62
+ }
63
+
64
+ /**
65
+ * Parse a string value to its proper type
66
+ */
67
+ export function parseValue(value) {
68
+ if (value === 'true') return true;
69
+ if (value === 'false') return false;
70
+ if (value === 'null') return null;
71
+ if (value === 'undefined') return undefined;
72
+ const num = Number(value);
73
+ if (!isNaN(num) && value !== '') return num;
74
+ return value;
75
+ }
76
+
77
+ /**
78
+ * Check if a value matches a search query (case-insensitive)
79
+ */
80
+ export function matchesSearch(value, query) {
81
+ if (value === null || value === undefined) return false;
82
+ return String(value).toLowerCase().includes(query.toLowerCase());
83
+ }
84
+
85
+ /**
86
+ * Wrap an async Express handler to catch errors
87
+ */
88
+ export function asyncHandler(fn) {
89
+ return (req, res, next) => {
90
+ Promise.resolve(fn(req, res, next)).catch(next);
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Send a standardized JSON response
96
+ */
97
+ export function sendResponse(res, statusCode, data, meta = null) {
98
+ const response = { success: statusCode >= 200 && statusCode < 300 };
99
+ if (meta) response.meta = meta;
100
+ if (data !== undefined) response.data = data;
101
+ return res.status(statusCode).json(response);
102
+ }
103
+
104
+ /**
105
+ * Send an error response
106
+ */
107
+ export function sendError(res, statusCode, message, errors = null) {
108
+ const response = {
109
+ success: false,
110
+ error: { status: statusCode, message },
111
+ };
112
+ if (errors) response.error.details = errors;
113
+ return res.status(statusCode).json(response);
114
+ }
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+
3
+ const PREFIX = chalk.hex('#a78bfa').bold('[PhantomBack]');
4
+
5
+ export const logger = {
6
+ info: (...args) => console.log(PREFIX, chalk.cyan('INFO'), ...args),
7
+ success: (...args) => console.log(PREFIX, chalk.green('✓'), ...args),
8
+ warn: (...args) => console.log(PREFIX, chalk.yellow('⚠'), ...args),
9
+ error: (...args) => console.error(PREFIX, chalk.red('✗'), ...args),
10
+ route: (method, path) => {
11
+ const colors = {
12
+ GET: chalk.green,
13
+ POST: chalk.blue,
14
+ PUT: chalk.yellow,
15
+ PATCH: chalk.magenta,
16
+ DELETE: chalk.red,
17
+ };
18
+ const colorFn = colors[method] || chalk.white;
19
+ console.log(PREFIX, colorFn.bold(method.padEnd(7)), chalk.dim(path));
20
+ },
21
+ server: (port) => {
22
+ console.log('');
23
+ console.log(PREFIX, chalk.green.bold('Server is running!'));
24
+ console.log(PREFIX, chalk.dim('Local:'), chalk.cyan.underline(`http://localhost:${port}`));
25
+ console.log('');
26
+ },
27
+ banner: () => {
28
+ console.log('');
29
+ console.log(chalk.hex('#a78bfa').bold(' ╔═══════════════════════════════════════╗'));
30
+ console.log(
31
+ chalk.hex('#a78bfa').bold(' ║ ') +
32
+ chalk.white.bold('PhantomBack v1.0.0') +
33
+ chalk.hex('#a78bfa').bold(' ║'),
34
+ );
35
+ console.log(
36
+ chalk.hex('#a78bfa').bold(' ║ ') +
37
+ chalk.dim('Instant Fake Backend Generator') +
38
+ chalk.hex('#a78bfa').bold(' ║'),
39
+ );
40
+ console.log(chalk.hex('#a78bfa').bold(' ╚═══════════════════════════════════════╝'));
41
+ console.log('');
42
+ },
43
+ table: (resources) => {
44
+ console.log(PREFIX, chalk.white.bold('Registered Resources:'));
45
+ for (const [name, config] of Object.entries(resources)) {
46
+ const count = config.seed || 0;
47
+ const auth = config.auth ? chalk.yellow(' 🔒') : '';
48
+ console.log(
49
+ PREFIX,
50
+ chalk.dim(' ├─'),
51
+ chalk.white.bold(name),
52
+ chalk.dim(`(${count} records)`),
53
+ auth,
54
+ );
55
+ }
56
+ console.log('');
57
+ },
58
+ };