@safagayret/bemirror 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/server.js ADDED
@@ -0,0 +1,322 @@
1
+ const express = require('express')
2
+ const cors = require('cors')
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+
6
+ const slugify = (str) => {
7
+ if (!str) return 'unknown'
8
+ return str
9
+ .toString()
10
+ .replace(/([a-z])([A-Z])/g, '$1-$2') // CamelCase / PascalCase to kebab-case
11
+ .toLowerCase()
12
+ .trim()
13
+ .replace(/[^\w\s-]/g, '')
14
+ .replace(/[\s_-]+/g, '-')
15
+ .replace(/^-+|-+$/g, '')
16
+ }
17
+
18
+ const replaceVariables = (str, vars) => {
19
+ if (!str || typeof str !== 'string') return str
20
+ return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
21
+ return vars[key] !== undefined ? vars[key] : match
22
+ })
23
+ }
24
+
25
+ const getDefaultData = () => ({ projects: [] })
26
+ const isPostPutPatch = (method) => ['POST', 'PUT', 'PATCH'].includes(method)
27
+ const isGetDelete = (method) => ['GET', 'DELETE'].includes(method)
28
+
29
+ const loadJsonFile = (filePath, defaultValue) => {
30
+ try {
31
+ if (fs.existsSync(filePath)) {
32
+ const fileContent = fs.readFileSync(filePath, 'utf-8')
33
+ return JSON.parse(fileContent)
34
+ }
35
+ } catch (err) {
36
+ console.warn(`Failed to load ${path.basename(filePath)}, starting with default state:`, err.message)
37
+ }
38
+ return defaultValue
39
+ }
40
+
41
+ const saveJsonFile = (filePath, data) => {
42
+ try {
43
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
44
+ } catch (err) {
45
+ console.error(`Failed to write ${path.basename(filePath)}:`, err.message)
46
+ }
47
+ }
48
+
49
+ const createBemirrorApp = (options = {}) => {
50
+ const ENDPOINTS_FILE = options.endpointsFile || path.join(__dirname, 'endpoints.json')
51
+ const VARIABLES_FILE = options.variablesFile || path.join(__dirname, 'variables.json')
52
+ const PUBLIC_DIR = options.publicDir || path.join(__dirname, 'public')
53
+
54
+ const app = express()
55
+ app.use(cors())
56
+ app.use(express.json({ limit: '10mb' }))
57
+ app.use(express.static(PUBLIC_DIR))
58
+
59
+ let memoryDb = getDefaultData()
60
+ let variables = {}
61
+
62
+ const getData = () => memoryDb
63
+ const saveData = (data) => {
64
+ memoryDb = data
65
+ saveJsonFile(ENDPOINTS_FILE, data)
66
+ }
67
+
68
+ const getVariables = () => variables
69
+ const saveVariables = (data) => {
70
+ variables = data
71
+ saveJsonFile(VARIABLES_FILE, data)
72
+ }
73
+
74
+ const loadState = () => {
75
+ memoryDb = loadJsonFile(ENDPOINTS_FILE, getDefaultData())
76
+ variables = loadJsonFile(VARIABLES_FILE, {})
77
+ }
78
+
79
+ loadState()
80
+
81
+ app.get('/api/data', (req, res) => {
82
+ res.json(getData())
83
+ })
84
+
85
+ app.post('/api/data', (req, res) => {
86
+ const incomingData = req.body
87
+ if (!incomingData || !Array.isArray(incomingData.projects)) {
88
+ return res.status(400).json({ error: 'Invalid data format' })
89
+ }
90
+ saveData(incomingData)
91
+ res.json({ success: true })
92
+ })
93
+
94
+ app.get('/api/variables', (req, res) => {
95
+ res.json(getVariables())
96
+ })
97
+
98
+ app.post('/api/variables', (req, res) => {
99
+ const incomingData = req.body
100
+ if (!incomingData || typeof incomingData !== 'object') {
101
+ return res.status(400).json({ error: 'Invalid variables format' })
102
+ }
103
+ saveVariables(incomingData)
104
+ res.json({ success: true })
105
+ })
106
+
107
+ app.post('/api/proxy', async (req, res) => {
108
+ const { url, method, params, payload } = req.body
109
+
110
+ if (!url) return res.status(400).json({ error: 'Missing Target URL' })
111
+
112
+ try {
113
+ const targetUrl = new URL(url)
114
+ if (params) {
115
+ try {
116
+ const queryObj = JSON.parse(params)
117
+ Object.keys(queryObj).forEach((k) => targetUrl.searchParams.append(k, queryObj[k]))
118
+ } catch (e) {}
119
+ }
120
+
121
+ const fetchOpts = {
122
+ method: method || 'GET',
123
+ headers: {},
124
+ }
125
+
126
+ if (payload && ['POST', 'PUT', 'PATCH'].includes(method)) {
127
+ fetchOpts.body = payload
128
+ fetchOpts.headers['Content-Type'] = 'application/json'
129
+ }
130
+
131
+ const startMs = Date.now()
132
+ let responseText = ''
133
+ let status = 500
134
+
135
+ try {
136
+ const response = await fetch(targetUrl.href, fetchOpts)
137
+ status = response.status
138
+ responseText = await response.text()
139
+ } catch (fetchErr) {
140
+ return res.status(502).json({ error: fetchErr.message })
141
+ }
142
+
143
+ const elapsedMs = Date.now() - startMs
144
+ let jsonFormatted = null
145
+ try {
146
+ jsonFormatted = JSON.parse(responseText)
147
+ } catch (e) {}
148
+
149
+ res.json({
150
+ status,
151
+ time: elapsedMs,
152
+ body: jsonFormatted || responseText,
153
+ })
154
+ } catch (err) {
155
+ res.status(500).json({ error: err.message })
156
+ }
157
+ })
158
+
159
+ app.use('/mock', (req, res) => {
160
+ const data = getData()
161
+ const vars = getVariables()
162
+ const requestPath = req.path
163
+ const requestMethod = req.method
164
+
165
+ const parts = requestPath.split('/').filter((p) => p.trim() !== '')
166
+ if (parts.length < 2) {
167
+ return res.status(404).json({
168
+ error:
169
+ 'Invalid URL structure. Expected /mock/[project-slug]/[entity-slug] or /mock/[project-slug]/[entity-slug]/[endpoint-path]',
170
+ })
171
+ }
172
+
173
+ const projSlug = parts[0]
174
+ const stringSegmentEntity = parts[1]
175
+ const remainingPath = parts.length >= 3 ? '/' + parts.slice(2).join('/') : '/'
176
+
177
+ const project = data.projects.find((p) => slugify(p.name) === projSlug)
178
+ if (!project) return res.status(404).json({ error: `Mock Project '${projSlug}' not found` })
179
+
180
+ let matchedEndpoint = null
181
+ for (const entity of project.entities || []) {
182
+ if (slugify(entity.name) !== stringSegmentEntity) continue
183
+
184
+ const ep = (entity.endpoints || []).find((e) => {
185
+ const cleanEpPath = e.path.startsWith('/') ? e.path : `/${e.path}`
186
+ return cleanEpPath === remainingPath && e.method === requestMethod
187
+ })
188
+
189
+ if (ep) {
190
+ matchedEndpoint = ep
191
+ break
192
+ }
193
+ }
194
+
195
+ if (!matchedEndpoint) {
196
+ return res.status(404).json({
197
+ error: 'Endpoint not found in this project/entity',
198
+ requestedPath: remainingPath,
199
+ requestMethod,
200
+ })
201
+ }
202
+
203
+ const processedEndpoint = {
204
+ ...matchedEndpoint,
205
+ path: replaceVariables(matchedEndpoint.path, vars),
206
+ expectedParams: replaceVariables(matchedEndpoint.expectedParams, vars),
207
+ expectedPayload: replaceVariables(matchedEndpoint.expectedPayload, vars),
208
+ responseBody: replaceVariables(matchedEndpoint.responseBody, vars),
209
+ headers: matchedEndpoint.headers
210
+ ? matchedEndpoint.headers.map((h) => ({
211
+ key: replaceVariables(h.key, vars),
212
+ value: replaceVariables(h.value, vars),
213
+ }))
214
+ : [],
215
+ authorization: matchedEndpoint.authorization
216
+ ? {
217
+ type: matchedEndpoint.authorization.type,
218
+ value: replaceVariables(matchedEndpoint.authorization.value, vars),
219
+ }
220
+ : { type: 'None', value: '' },
221
+ }
222
+
223
+ if (isPostPutPatch(requestMethod) && processedEndpoint.expectedPayload) {
224
+ try {
225
+ const expectedObj = JSON.parse(processedEndpoint.expectedPayload)
226
+ const incomingObj = req.body || {}
227
+ const missingKeys = []
228
+ Object.keys(expectedObj).forEach((key) => {
229
+ if (incomingObj[key] === undefined) missingKeys.push(key)
230
+ })
231
+ if (missingKeys.length > 0)
232
+ return res.status(400).json({ error: 'Validation failed', missing_expected_keys: missingKeys })
233
+ } catch (e) {}
234
+ }
235
+
236
+ if (isGetDelete(requestMethod) && processedEndpoint.expectedParams) {
237
+ try {
238
+ const expectedParams = JSON.parse(processedEndpoint.expectedParams)
239
+ const incomingParams = req.query || {}
240
+ const missingParams = []
241
+ Object.keys(expectedParams).forEach((key) => {
242
+ if (incomingParams[key] === undefined) missingParams.push(key)
243
+ })
244
+ if (missingParams.length > 0)
245
+ return res.status(400).json({ error: 'Validation failed', missing_query_parameters: missingParams })
246
+ } catch (e) {}
247
+ }
248
+
249
+ let responseJson = {}
250
+ let isJson = true
251
+ try {
252
+ responseJson = processedEndpoint.responseBody ? JSON.parse(processedEndpoint.responseBody) : {}
253
+ } catch (e) {
254
+ responseJson = processedEndpoint.responseBody
255
+ isJson = false
256
+ }
257
+
258
+ const statusCode = parseInt(processedEndpoint.statusCode, 10) || 200
259
+ const delay = parseInt(processedEndpoint.delay, 10) || 0
260
+
261
+ setTimeout(() => {
262
+ if (processedEndpoint.headers && Array.isArray(processedEndpoint.headers)) {
263
+ processedEndpoint.headers.forEach((h) => {
264
+ if (h.key && h.value) {
265
+ res.setHeader(h.key, h.value)
266
+ }
267
+ })
268
+ }
269
+
270
+ if (processedEndpoint.authorization && processedEndpoint.authorization.type !== 'None') {
271
+ console.log(
272
+ `Authorization: ${processedEndpoint.authorization.type} - ${processedEndpoint.authorization.value}`,
273
+ )
274
+ }
275
+
276
+ res.status(statusCode)
277
+ if (isJson) {
278
+ res.setHeader('Content-Type', 'application/json')
279
+ res.send(JSON.stringify(responseJson, null, 2))
280
+ } else {
281
+ res.setHeader('Content-Type', 'text/plain')
282
+ res.send(responseJson)
283
+ }
284
+ }, delay)
285
+ })
286
+
287
+ app.use((req, res) => {
288
+ res.sendFile(path.join(PUBLIC_DIR, 'index.html'))
289
+ })
290
+
291
+ return {
292
+ app,
293
+ getData,
294
+ saveData,
295
+ getVariables,
296
+ saveVariables,
297
+ }
298
+ }
299
+
300
+ const startBemirrorServer = (options = {}) => {
301
+ const { app } = createBemirrorApp(options)
302
+ const port = options.port || process.env.PORT || 8000
303
+ const server = app.listen(port, () => {
304
+ console.log(`bemirror Micro Server is running on http://localhost:${port}`)
305
+ })
306
+
307
+ return {
308
+ app,
309
+ server,
310
+ close: () => server.close(),
311
+ port,
312
+ }
313
+ }
314
+
315
+ if (require.main === module) {
316
+ startBemirrorServer()
317
+ }
318
+
319
+ module.exports = {
320
+ createBemirrorApp,
321
+ startBemirrorServer,
322
+ }