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.
- package/LICENSE +21 -0
- package/README.md +459 -0
- package/bin/phantomback.js +39 -0
- package/package.json +63 -0
- package/src/cli/commands.js +138 -0
- package/src/data/relations.js +59 -0
- package/src/data/seeder.js +190 -0
- package/src/data/store.js +181 -0
- package/src/features/auth.js +123 -0
- package/src/features/delay.js +40 -0
- package/src/features/filters.js +84 -0
- package/src/features/pagination.js +38 -0
- package/src/features/search.js +18 -0
- package/src/features/sorting.js +47 -0
- package/src/index.js +67 -0
- package/src/schema/defaults.js +62 -0
- package/src/schema/parser.js +94 -0
- package/src/schema/validator.js +153 -0
- package/src/server/createServer.js +57 -0
- package/src/server/middleware.js +64 -0
- package/src/server/router.js +274 -0
- package/src/utils/helpers.js +114 -0
- package/src/utils/logger.js +58 -0
|
@@ -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
|
+
};
|