UNPKG

45.6 kBJavaScriptView Raw
1#!/usr/bin/env node
2import Discover from 'node-discover';
3import yargs from 'yargs';
4import { resolve, join, sep } from 'path';
5import { readFile } from 'fs/promises';
6import { hideBin } from 'yargs/helpers';
7import ansiColors from 'ansi-colors';
8import { getResponseHeader, fromNodeMiddleware, handleCors, getRequestHeader, eventHandler, setResponseHeader, getRouterParams, getQuery, send, toNodeListener, createApp, createRouter, readBody } from 'h3';
9import { listen } from 'listhen';
10import mergician from 'mergician';
11import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
12import serveStatic$1 from 'serve-static';
13import morgan$1, { token } from 'morgan';
14import { promises } from 'fs';
15import { replace } from '@jota-one/replacer';
16import { async } from 'rrdir';
17import { v4 } from 'uuid';
18import { createRequire } from 'module';
19import Loki from 'lokijs';
20import _vorpal from '@moleculer/vorpal';
21
22function cleanIndexInpath(pathPart) {
23 return pathPart.startsWith("[") ? pathPart.replace(/\D/g, "") : pathPart;
24}
25function getObjectPaths(paths) {
26 return [].concat(paths).map(
27 (path) => path.split(".").map(cleanIndexInpath).join(".")
28 );
29}
30function curry(fn) {
31 return function(...args) {
32 return fn.bind(null, ...args);
33 };
34}
35function isEmpty(obj) {
36 if (!obj) {
37 return true;
38 }
39 return !Object.keys(obj).length;
40}
41function get(obj, path, defaultValue) {
42 const result = path.split(".").reduce((r, p) => {
43 if (typeof r === "object" && r !== null) {
44 p = cleanIndexInpath(p);
45 return r[p];
46 }
47 return void 0;
48 }, obj);
49 return result === void 0 ? defaultValue : result;
50}
51function omit(obj, paths) {
52 if (obj === null) {
53 return null;
54 }
55 if (!obj) {
56 return obj;
57 }
58 if (typeof obj !== "object") {
59 return obj;
60 }
61 const buildObj = (obj2, paths2, _path = "", _result) => {
62 const result = !_result ? Array.isArray(obj2) ? [] : {} : _result;
63 for (const [key, value] of Object.entries(obj2)) {
64 if (paths2.includes(key)) {
65 continue;
66 }
67 if (typeof value === "object" && value !== null) {
68 result[key] = buildObj(
69 value,
70 paths2,
71 `${_path}${key}.`,
72 Array.isArray(value) ? [] : {}
73 );
74 } else {
75 result[key] = value;
76 }
77 }
78 return result;
79 };
80 return buildObj(obj, getObjectPaths(paths));
81}
82function pick(obj, paths) {
83 const result = {};
84 for (const path of getObjectPaths(paths)) {
85 const value = get(obj, path);
86 if (value) {
87 set(result, path, value);
88 }
89 }
90 return result;
91}
92function set(obj, path, val) {
93 path.split && (path = path.split("."));
94 let i = 0;
95 const l = path.length;
96 let t = obj;
97 let x;
98 let k;
99 while (i < l) {
100 k = path[i++];
101 if (k === "__proto__" || k === "constructor" || k === "prototype")
102 break;
103 t = t[k] = i === l ? val : typeof (x = t[k]) === typeof path && t[k] !== null ? x : path[i] * 0 !== 0 || !!~("" + path[i]).indexOf(".") ? {} : [];
104 }
105}
106function cloneDeep(obj) {
107 return mergician({}, obj);
108}
109function merge(obj1, obj2) {
110 return mergician(obj1, obj2);
111}
112
113const RESTART_DISABLED_IN_ESM_MODE = "Restart is not supported in esm mode.";
114
115const config = {
116 db: {
117 reservedFields: ["DROSSE", "meta", "$loki"]
118 },
119 state: {
120 assetsPath: "assets",
121 baseUrl: "",
122 basePath: "",
123 collectionsPath: "collections",
124 database: "mocks.json",
125 dbAdapter: "LokiFsAdapter",
126 name: "Drosse mock server",
127 port: 8e3,
128 reservedRoutes: { ui: "/UI", cmd: "/CMD" },
129 routesFile: "routes",
130 scrapedPath: "scraped",
131 scraperServicesPath: "scrapers",
132 servicesPath: "services",
133 shallowCollections: [],
134 staticPath: "static",
135 uuid: ""
136 },
137 cli: null,
138 commands: {},
139 errorHandler: null,
140 extendServer: null,
141 middlewares: [],
142 templates: {},
143 onHttpUpgrade: null
144};
145
146function Logger() {
147 this.debug = function(...args) {
148 log("white", args);
149 };
150 this.success = function(...args) {
151 log("green", args);
152 };
153 this.info = function(...args) {
154 log("cyan", args);
155 };
156 this.warn = function(...args) {
157 log("yellow", args);
158 };
159 this.error = function(...args) {
160 log("red", args);
161 };
162}
163function getTime() {
164 return new Date().toLocaleTimeString();
165}
166function log(color, args) {
167 args = args.map((arg) => {
168 if (typeof arg === "object") {
169 return JSON.stringify(arg);
170 }
171 return arg;
172 });
173 console.log(ansiColors.gray(getTime()), ansiColors[color](args.join(" ")));
174}
175const logger = new Logger();
176
177const rewriteLinks$1 = (links) => Object.entries(links).reduce((links2, [key, link]) => {
178 let { href } = link;
179 if (href.indexOf("http") === 0) {
180 href = `/${href.split("/").slice(3).join("/")}`;
181 }
182 links2[key] = { ...link, href };
183 return links2;
184}, {});
185const walkObj$1 = (root = {}, prefix = "") => {
186 if (typeof root !== "object") {
187 return root;
188 }
189 const obj = (prefix ? get(root, prefix) : root) || {};
190 Object.entries(obj).forEach(([key, value]) => {
191 const path = `${prefix && prefix + "."}${key}`;
192 if (key === "_links") {
193 set(root, path, rewriteLinks$1(value));
194 }
195 walkObj$1(root, path);
196 });
197};
198function halLinks(body) {
199 walkObj$1(body);
200 return body;
201}
202
203const rewriteLinks = (links) => links.map((link) => {
204 let { href } = link;
205 if (href.indexOf("http") === 0) {
206 href = `/${href.split("/").slice(3).join("/")}`;
207 }
208 return { ...link, href };
209});
210const walkObj = (root = {}, prefix = "") => {
211 if (typeof root !== "object") {
212 return root;
213 }
214 const obj = (prefix ? get(root, prefix) : root) || {};
215 Object.entries(obj).forEach(([key, value]) => {
216 const path = `${prefix && prefix + "."}${key}`;
217 if (key === "links") {
218 const linkKeys = Object.keys(value[0] || []);
219 if (linkKeys.includes("href") && linkKeys.includes("rel")) {
220 set(root, path, rewriteLinks(value));
221 } else {
222 walkObj(root, path);
223 }
224 }
225 walkObj(root, path);
226 });
227};
228function hateoasLinks(body) {
229 walkObj(body);
230 return body;
231}
232
233let state$9 = JSON.parse(JSON.stringify(config.state));
234function useState() {
235 return {
236 set(key, value) {
237 state$9[key] = value;
238 },
239 get(key) {
240 if (key) {
241 return state$9[key];
242 }
243 return state$9;
244 },
245 merge(conf) {
246 const keysWhitelist = Object.keys(state$9);
247 state$9 = merge(state$9, pick(conf, keysWhitelist));
248 }
249 };
250}
251
252const state$8 = useState();
253token("time", function getTime() {
254 return ansiColors.gray(new Date().toLocaleTimeString());
255});
256token("status", function(req, res) {
257 const col = color(res.statusCode);
258 return ansiColors[col](res.statusCode);
259});
260token("method", function(req, res) {
261 const verb = req.method.padEnd(7);
262 const col = color(res.statusCode);
263 return ansiColors[col](verb);
264});
265token("url", function(req, res) {
266 const url = req.originalUrl || req.url;
267 return getResponseHeader(res, "x-proxied") ? ansiColors.cyan(url) : ansiColors[color(res.statusCode)](url);
268});
269token("proxied", function(req, res) {
270 return getResponseHeader(res, "x-proxied") ? ansiColors.cyanBright("\u{1F500} proxied") : "";
271});
272const color = (status) => {
273 if (status >= 400) {
274 return "red";
275 }
276 if (status >= 300) {
277 return "yellow";
278 }
279 return "green";
280};
281const format = function(tokens, req, res) {
282 return [
283 tokens.time(req, res),
284 tokens.method(req, res),
285 tokens.status(req, res),
286 "-",
287 tokens["response-time"](req, res, 0) ? tokens["response-time"](req, res, 0).concat("ms").padEnd(7) : "\u{1F6AB}",
288 tokens.url(req, res),
289 tokens.proxied(req, res)
290 ].join(" ");
291};
292const morgan = fromNodeMiddleware(
293 morgan$1(format, {
294 skip: (req) => Object.values(state$8.get("reservedRoutes")).includes(req.url)
295 })
296);
297
298function openCors(event) {
299 handleCors(event, {
300 origin: getRequestHeader(event, "origin") || "*",
301 methods: [
302 "GET",
303 "PUT",
304 "POST",
305 "PATCH",
306 "DELETE",
307 "OPTIONS",
308 "HEAD",
309 "CONNECT"
310 ],
311 allowHeaders: "*",
312 credentials: true,
313 preflight: {
314 statusCode: 204
315 }
316 });
317}
318
319const internalMiddlewares = {
320 "hal-links": halLinks,
321 "hateoas-links": hateoasLinks,
322 morgan,
323 "open-cors": openCors
324};
325
326let cjsRequire;
327const esmMode = import.meta.url.endsWith(".mjs") || process.argv[1].endsWith(".mjs");
328const getFullPath = (path) => {
329 const state = useState();
330 const root = state.get("root") || __dirname;
331 return resolve(root, path);
332};
333const load$1 = async function(path) {
334 let module;
335 const fullPath = getFullPath(path);
336 if (esmMode) {
337 module = (await import(fullPath)).default;
338 } else {
339 if (!cjsRequire) {
340 cjsRequire = createRequire(import.meta.url);
341 }
342 delete require.cache[fullPath];
343 module = require(fullPath);
344 }
345 return module;
346};
347const isEsmMode$1 = function() {
348 return esmMode;
349};
350function useIO$1() {
351 return { isEsmMode: isEsmMode$1, load: load$1 };
352}
353
354const state$7 = useState();
355const { load } = useIO$1();
356const fileExists = async (path) => {
357 let exists;
358 try {
359 await promises.access(path);
360 exists = true;
361 } catch {
362 exists = false;
363 }
364 return exists;
365};
366const checkRoutesFile$1 = async () => {
367 const filePath = join(
368 state$7.get("root"),
369 `${state$7.get("routesFile")}.json`
370 );
371 if (await fileExists(filePath)) {
372 state$7.set("_routesFile", filePath);
373 return true;
374 }
375 return false;
376};
377const getUserConfig$1 = async (root) => {
378 const rcFilePath = join(root || state$7.get("root") || "", ".drosserc.js");
379 try {
380 await promises.stat(rcFilePath);
381 return load(rcFilePath);
382 } catch (e) {
383 console.error("Could not load any user config.");
384 console.error(e);
385 return {};
386 }
387};
388const loadService$1 = async (routePath, verb) => {
389 const serviceFile = join(
390 state$7.get("root"),
391 state$7.get("servicesPath"),
392 routePath.filter((el) => el[0] !== ":").join(".")
393 ) + `.${verb}.js`;
394 if (!await fileExists(serviceFile)) {
395 return function() {
396 logger.error(`service [${serviceFile}] not found`);
397 };
398 }
399 const service = await load(serviceFile);
400 return { serviceFile, service };
401};
402const loadScraperService$1 = async (routePath) => {
403 const serviceFile = join(
404 state$7.get("root"),
405 state$7.get("scraperServicesPath"),
406 routePath.filter((el) => el[0] !== ":").join(".")
407 ) + ".js";
408 if (!await fileExists(serviceFile)) {
409 return function() {
410 logger.error(`scraper service [${serviceFile}] not found`);
411 };
412 }
413 return load(serviceFile);
414};
415const writeScrapedFile = async (filename, content) => {
416 const root = join(state$7.get("root"), state$7.get("scrapedPath"));
417 await promises.writeFile(
418 join(root, filename),
419 JSON.stringify(content),
420 "utf-8"
421 );
422 return true;
423};
424const loadStatic$2 = async ({
425 routePath,
426 params = {},
427 query = {},
428 verb = null,
429 skipVerb = false,
430 extensions = ["json"]
431}) => {
432 const root = join(state$7.get("root"), state$7.get("staticPath"));
433 const files = await async(root);
434 return findStatic({ root, files, routePath, params, verb, skipVerb, query, extensions });
435};
436const loadScraped$2 = async ({
437 routePath,
438 params = {},
439 query = {},
440 verb = null,
441 skipVerb = false,
442 extensions = ["json"]
443}) => {
444 const root = join(state$7.get("root"), state$7.get("scrapedPath"));
445 const files = await async(root);
446 return findStatic({ root, files, extensions, routePath, params, verb, skipVerb, query });
447};
448const loadUuid$1 = async () => {
449 const uuidFile = join(state$7.get("root"), ".uuid");
450 if (!await fileExists(uuidFile)) {
451 await promises.writeFile(uuidFile, v4(), "utf8");
452 }
453 const uuid = await promises.readFile(uuidFile, "utf8");
454 state$7.merge({ uuid });
455};
456const getRoutesDef$1 = async () => {
457 const content = await promises.readFile(state$7.get("_routesFile"), "utf8");
458 return JSON.parse(content);
459};
460const getStaticFileName = (routePath, extension, params = {}, verb = null, query = {}) => {
461 const queryPart = Object.entries(query).sort(([name1], [name2]) => {
462 return name1 > name2 ? 1 : -1;
463 }).reduce((acc, [name, value]) => {
464 return acc.concat(`${name}=${value}`);
465 }, []).join("&");
466 let filename = replace(
467 routePath.join(".").concat(verb ? `.${verb.toLowerCase()}` : "").replace(/:([^\\/\\.]+)/gim, "{$1}").concat(queryPart ? `&&${queryPart}` : ""),
468 params
469 );
470 const extensionLength = extension.length;
471 return filename.concat(filename.slice(-(extensionLength + 1)) === `.${extension}` ? "" : `.${extension}`);
472};
473const findStatic = async ({
474 root,
475 files,
476 routePath,
477 extensions,
478 params = {},
479 verb = null,
480 skipVerb = false,
481 query = {},
482 initial = null,
483 extensionIndex = 0
484}) => {
485 const normalizedPath = (filePath) => filePath.replace(root, "").substring(1);
486 if (initial === null) {
487 initial = cloneDeep({
488 params,
489 query,
490 verb,
491 skipVerb
492 });
493 }
494 const filename = getStaticFileName(
495 routePath,
496 extensions[extensionIndex],
497 params,
498 !skipVerb && verb,
499 query
500 );
501 let staticFile = join(root, filename);
502 const foundFiles = files.filter(
503 (file) => normalizedPath(file.path).replace(/\//gim, ".") === normalizedPath(staticFile)
504 );
505 if (foundFiles.length > 1) {
506 const error = `findStatic: more than 1 file found for:
507[${staticFile}]:
508${foundFiles.map((f) => f.path).join("\n")}`;
509 throw new Error(error);
510 }
511 if (foundFiles.length === 0) {
512 if (!isEmpty(query)) {
513 logger.error(`findStatic: tried with [${staticFile}]. File not found.`);
514 return findStatic({
515 root,
516 files,
517 routePath,
518 params,
519 verb,
520 skipVerb,
521 extensions,
522 initial,
523 extensionIndex
524 });
525 }
526 if (verb && !skipVerb) {
527 logger.error(`findStatic: tried with [${staticFile}]. File not found.`);
528 return findStatic({
529 root,
530 files,
531 routePath,
532 params,
533 verb,
534 skipVerb: true,
535 query,
536 extensions,
537 initial,
538 extensionIndex
539 });
540 }
541 if (!isEmpty(params)) {
542 logger.error(`findStatic: tried with [${staticFile}]. File not found.`);
543 const newParams = Object.keys(params).slice(1).reduce((acc, name) => ({ ...acc, [name]: params[name] }), {});
544 return findStatic({
545 root,
546 files,
547 routePath,
548 params: newParams,
549 verb,
550 extensions,
551 initial,
552 extensionIndex
553 });
554 }
555 if (extensionIndex === extensions.length - 1) {
556 logger.error(`findStatic: I think I've tried everything. No match...`);
557 return [false, false];
558 } else {
559 logger.warn(`findStatic: Okay, tried everything with ${extensions[extensionIndex]} extension. Let's try with the next one.`);
560 return findStatic({
561 root,
562 files,
563 routePath,
564 params: cloneDeep(initial.params),
565 query: cloneDeep(initial.query),
566 verb: initial.verb,
567 skipVerb: initial.skipVerb,
568 extensions,
569 initial,
570 extensionIndex: extensionIndex + 1
571 });
572 }
573 } else {
574 const foundExtension = extensions[extensionIndex];
575 staticFile = foundFiles[0].path;
576 logger.info(`findStatic: file used: ${staticFile}`);
577 if (foundExtension === "json") {
578 const fileContent = await promises.readFile(staticFile, "utf-8");
579 const result = replace(fileContent, initial.params);
580 return [JSON.parse(result), foundExtension];
581 }
582 return [staticFile, foundExtension];
583 }
584};
585function useIO() {
586 return {
587 checkRoutesFile: checkRoutesFile$1,
588 getRoutesDef: getRoutesDef$1,
589 getStaticFileName,
590 getUserConfig: getUserConfig$1,
591 loadService: loadService$1,
592 loadScraperService: loadScraperService$1,
593 loadStatic: loadStatic$2,
594 loadScraped: loadScraped$2,
595 loadUuid: loadUuid$1,
596 writeScrapedFile
597 };
598}
599
600let db$2;
601function useDB() {
602 const state = useState();
603 const collectionsPath = () => join(state.get("root"), state.get("collectionsPath"));
604 const normalizedPath = (filePath) => filePath.replace(collectionsPath(), "").substr(1);
605 const loadAllMockFiles = async () => {
606 const res = await async(collectionsPath());
607 return res.filter((entry) => !entry.directory && entry.path.endsWith("json")).map((entry) => {
608 entry.path = normalizedPath(entry.path);
609 return entry;
610 });
611 };
612 const handleCollection = (name, alreadyHandled, newCollections) => {
613 const shallowCollections = state.get("shallowCollections");
614 const coll = db$2.getCollection(name);
615 if (coll) {
616 if (newCollections.includes(name)) {
617 return coll;
618 }
619 if (!shallowCollections.includes(name)) {
620 if (alreadyHandled.includes(name)) {
621 return false;
622 }
623 logger.warn(
624 "\u{1F4E6} collection",
625 name,
626 "already exists and won't be overriden."
627 );
628 alreadyHandled.push(name);
629 return false;
630 }
631 if (alreadyHandled.includes(name)) {
632 return coll;
633 }
634 logger.warn(
635 "\u{1F30A} collection",
636 name,
637 "already exists and will be overriden."
638 );
639 db$2.removeCollection(name);
640 alreadyHandled.push(name);
641 }
642 newCollections.push(name);
643 return db$2.addCollection(name);
644 };
645 const loadContents = async () => {
646 const files = await loadAllMockFiles();
647 const handled = [];
648 const newCollections = [];
649 for (const entry of files) {
650 const filename = entry.path.split(sep).pop();
651 const fileContent = await promises.readFile(
652 join(collectionsPath(), entry.path),
653 "utf-8"
654 );
655 const content = JSON.parse(fileContent);
656 const collectionName = Array.isArray(content) ? entry.path.slice(0, -5).split(sep).join(".") : entry.path.split(sep).slice(0, -1).join(".");
657 const coll = handleCollection(collectionName, handled, newCollections);
658 if (coll) {
659 coll.insert(content);
660 logger.success(`loaded ${filename} into collection ${collectionName}`);
661 }
662 }
663 };
664 const clean = (...fields) => (result) => omit(result, config.db.reservedFields.concat(fields || []));
665 const service = {
666 loadDb() {
667 return new Promise((resolve, reject) => {
668 try {
669 const AdapterName = state.get("dbAdapter");
670 const adapter = Loki[AdapterName] ? new Loki[AdapterName]() : new AdapterName();
671 db$2 = new Loki(join(state.get("root"), state.get("database")), {
672 adapter,
673 autosave: true,
674 autosaveInterval: 4e3,
675 autoload: true,
676 autoloadCallback: () => {
677 loadContents().then(() => resolve(db$2));
678 }
679 });
680 } catch (e) {
681 reject(e);
682 }
683 });
684 },
685 loki: function() {
686 return db$2;
687 },
688 collection: function(name) {
689 let coll = db$2.getCollection(name);
690 if (!coll) {
691 coll = db$2.addCollection(name);
692 }
693 return coll;
694 },
695 list: {
696 all(collection, cleanFields = []) {
697 const coll = service.collection(collection);
698 return coll.data.map(clean(...cleanFields));
699 },
700 byId(collection, id, cleanFields = []) {
701 const coll = service.collection(collection);
702 return coll.find({ "DROSSE.ids": { $contains: id } }).map(clean(...cleanFields));
703 },
704 byField(collection, field, value, cleanFields = []) {
705 return this.byFields(collection, [field], value, cleanFields);
706 },
707 byFields(collection, fields, value, cleanFields = []) {
708 return this.find(
709 collection,
710 {
711 $or: fields.map((field) => ({
712 [field]: { $contains: value }
713 }))
714 },
715 cleanFields
716 );
717 },
718 find(collection, query, cleanFields = []) {
719 const coll = service.collection(collection);
720 return coll.chain().find(query).data().map(clean(...cleanFields));
721 },
722 where(collection, searchFn, cleanFields = []) {
723 const coll = service.collection(collection);
724 return coll.chain().where(searchFn).data().map(clean(...cleanFields));
725 }
726 },
727 get: {
728 byId(collection, id, cleanFields = []) {
729 const coll = service.collection(collection);
730 return clean(...cleanFields)(
731 coll.findOne({ "DROSSE.ids": { $contains: id } })
732 );
733 },
734 byRef(refObj, dynamicId, cleanFields = []) {
735 const { collection, id: refId } = refObj;
736 const id = dynamicId || refId;
737 return {
738 ...this.byId(collection, id, cleanFields),
739 ...omit(refObj, ["collection", "id"])
740 };
741 },
742 byField(collection, field, value, cleanFields = []) {
743 return this.byFields(collection, [field], value, cleanFields);
744 },
745 byFields(collection, fields, value, cleanFields = []) {
746 return this.find(
747 collection,
748 {
749 $or: fields.map((field) => ({
750 [field]: { $contains: value }
751 }))
752 },
753 cleanFields
754 );
755 },
756 find(collection, query, cleanFields = []) {
757 const coll = service.collection(collection);
758 return clean(...cleanFields)(coll.findOne(query));
759 },
760 where(collection, searchFn, cleanFields = []) {
761 const result = service.list.where(collection, searchFn, cleanFields);
762 if (result.length > 0) {
763 return result[0];
764 }
765 return null;
766 }
767 },
768 query: {
769 getIdMap(collection, fieldname, firstOnly = false) {
770 const coll = service.collection(collection);
771 return coll.data.reduce(
772 (acc, item) => ({
773 ...acc,
774 [item[fieldname]]: firstOnly ? item.DROSSE.ids[0] : item.DROSSE.ids
775 }),
776 {}
777 );
778 },
779 chain(collection) {
780 return service.collection(collection).chain();
781 },
782 clean
783 },
784 insert(collection, ids, payload) {
785 const coll = service.collection(collection);
786 return coll.insert(cloneDeep({ ...payload, DROSSE: { ids } }));
787 },
788 update: {
789 byId(collection, id, newValue) {
790 const coll = service.collection(collection);
791 coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => {
792 Object.entries(newValue).forEach(([key, value]) => {
793 set(doc, key, value);
794 });
795 });
796 },
797 subItem: {
798 append(collection, id, subPath, payload) {
799 const coll = service.collection(collection);
800 coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => {
801 if (!get(doc, subPath)) {
802 set(doc, subPath, []);
803 }
804 get(doc, subPath).push(payload);
805 });
806 },
807 prepend(collection, id, subPath, payload) {
808 const coll = service.collection(collection);
809 coll.findAndUpdate({ "DROSSE.ids": { $contains: id } }, (doc) => {
810 if (!get(doc, subPath)) {
811 set(doc, subPath, []);
812 }
813 get(doc, subPath).unshift(payload);
814 });
815 }
816 }
817 },
818 remove: {
819 byId(collection, id) {
820 const coll = service.collection(collection);
821 const toDelete = coll.findOne({ "DROSSE.ids": { $contains: id } });
822 return toDelete && coll.remove(toDelete);
823 }
824 }
825 };
826 return service;
827}
828
829const db$1 = useDB();
830const state$6 = useState();
831const { loadStatic: loadStatic$1, loadScraped: loadScraped$1 } = useIO();
832function useAPI(event) {
833 return {
834 event,
835 db: db$1,
836 logger,
837 io: { loadStatic: loadStatic$1, loadScraped: loadScraped$1 },
838 config: state$6.get()
839 };
840}
841
842const state$5 = useState();
843const parse$1 = async ({ routes, root = [], hierarchy = [], onRouteDef }) => {
844 let inherited = [];
845 const localHierarchy = [].concat(hierarchy);
846 if (routes.DROSSE) {
847 localHierarchy.push({ ...routes.DROSSE, path: root });
848 }
849 const orderedRoutes = Object.entries(routes).filter(([path]) => path !== "DROSSE");
850 for (const orderedRoute of orderedRoutes) {
851 const [path, content] = orderedRoute;
852 const fullPath = `/${root.join("/")}`;
853 if (Object.values(state$5.get("reservedRoutes")).includes(fullPath)) {
854 throw new Error(`Route "${fullPath}" is reserved`);
855 }
856 const parsed = await parse$1({
857 routes: content,
858 root: root.concat(path),
859 hierarchy: localHierarchy,
860 onRouteDef
861 });
862 inherited = inherited.concat(parsed);
863 }
864 if (routes.DROSSE) {
865 const routeDef = await onRouteDef(routes.DROSSE, root, localHierarchy);
866 inherited = inherited.concat(routeDef);
867 }
868 return inherited;
869};
870function useParser() {
871 return { parse: parse$1 };
872}
873
874function useScraper() {
875 const { getStaticFileName, writeScrapedFile } = useIO();
876 const staticService = async (json, api) => {
877 const { req } = api;
878 const filename = getStaticFileName(
879 req.baseUrl.split("/").slice(1),
880 "json",
881 req.params,
882 req.method,
883 req.query
884 );
885 await writeScrapedFile(filename, json);
886 return true;
887 };
888 return {
889 staticService
890 };
891}
892
893let state$4 = config.templates;
894function useTemplates() {
895 return {
896 merge(tpls) {
897 state$4 = { ...state$4, ...tpls };
898 },
899 set(tpl) {
900 state$4 = tpl;
901 },
902 list() {
903 return state$4;
904 }
905 };
906}
907
908const { loadService, loadScraperService, loadStatic, loadScraped } = useIO();
909const { parse } = useParser();
910const state$3 = useState();
911const templates = useTemplates();
912const getThrottle = function(min, max) {
913 return Math.floor(Math.random() * (max - min)) + min;
914};
915const getThrottleMiddleware = (def) => eventHandler(
916 () => new Promise((resolve) => {
917 const delay = getThrottle(
918 def.throttle.min || 0,
919 def.throttle.max || def.throttle.min
920 );
921 setTimeout(resolve, delay);
922 })
923);
924const getProxy = function(def) {
925 return typeof def.proxy === "string" ? { target: def.proxy } : def.proxy;
926};
927const createRoutes = async (app, router, routes) => {
928 const context = { app, router, proxies: [], assets: [] };
929 const inherited = await parse({
930 routes,
931 onRouteDef: await createRoute.bind(context)
932 });
933 const result = inherited.reduce((acc, item) => {
934 if (!acc[item.path]) {
935 acc[item.path] = {};
936 }
937 if (!acc[item.path][item.verb]) {
938 acc[item.path][item.verb] = {
939 template: false,
940 throttle: false,
941 proxy: false
942 };
943 }
944 acc[item.path][item.verb][item.type] = true;
945 return acc;
946 }, {});
947 createAssets(context);
948 createProxies(context);
949 return result;
950};
951const createRoute = async function(def, root, defHierarchy) {
952 const { router, app, proxies, assets } = this;
953 const inheritance = [];
954 const verbs = ["get", "post", "put", "delete"].filter((verb) => def[verb]);
955 for (const verb of verbs) {
956 const originalThrottle = !isEmpty(def[verb].throttle);
957 def[verb].throttle = def[verb].throttle || defHierarchy.reduce((acc, item) => item.throttle || acc, {});
958 if (!originalThrottle && !isEmpty(def[verb].throttle)) {
959 inheritance.push({ path: root.join("/"), type: "throttle", verb });
960 }
961 const originalTemplate = def[verb].template === null || Boolean(def[verb].template);
962 def[verb].template = originalTemplate ? def[verb].template : defHierarchy.reduce((acc, item) => item.template || acc, {});
963 if (!originalTemplate && Object.keys(def[verb].template).length) {
964 inheritance.push({ path: root.join("/"), type: "template", verb });
965 }
966 const inheritsProxy = Boolean(
967 defHierarchy.find((item) => root.join("/").includes(item.path.join("/")))?.proxy
968 );
969 await setRoute(app, router, def[verb], verb, root, inheritsProxy);
970 }
971 if (def.assets) {
972 const routePath = [""].concat(root);
973 const assetsSubPath = def.assets === true ? routePath : typeof def.assets === "string" ? def.assets.split("/") : def.assets;
974 assets.push({
975 path: routePath.join("/"),
976 context: { target: join(state$3.get("assetsPath"), ...assetsSubPath) }
977 });
978 }
979 if (def.proxy || def.scraper) {
980 const proxyResHooks = [];
981 const path = [""].concat(root);
982 const onProxyReq = async function(proxyReq, req, res) {
983 return new Promise((resolve, reject) => {
984 try {
985 if (!isEmpty(req.body)) {
986 const bodyData = JSON.stringify(req.body);
987 proxyReq.setHeader("Content-Type", "application/json");
988 proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData));
989 proxyReq.write(bodyData);
990 }
991 resolve();
992 } catch (e) {
993 reject(e);
994 }
995 });
996 };
997 const applyProxyRes = function(hooks, def2) {
998 if (hooks.length === 0) {
999 return;
1000 }
1001 return responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
1002 const response = responseBuffer.toString("utf8");
1003 try {
1004 const json = JSON.parse(response);
1005 hooks.forEach((hook) => hook(json, req, res));
1006 return JSON.stringify(json);
1007 } catch (e) {
1008 console.log("Response error: could not encode string to JSON");
1009 console.log(response);
1010 console.log(e);
1011 console.log(
1012 "Will try to fallback on the static mock or at least return a vaild JSON string."
1013 );
1014 return JSON.stringify(def2.body) || "{}";
1015 }
1016 });
1017 };
1018 if (def.scraper) {
1019 let tmpProxy = def.proxy;
1020 if (!tmpProxy) {
1021 tmpProxy = defHierarchy.reduce((acc, item) => {
1022 if (item.proxy) {
1023 return {
1024 proxy: getProxy(item),
1025 path: item.path
1026 };
1027 } else {
1028 if (!acc) {
1029 return acc;
1030 }
1031 const subpath = item.path.filter((path2) => !acc.path.includes(path2));
1032 const proxy = getProxy(acc);
1033 proxy.target = proxy.target.split("/").concat(subpath).join("/");
1034 return {
1035 proxy,
1036 path: item.path
1037 };
1038 }
1039 }, null);
1040 def.proxy = tmpProxy && tmpProxy.proxy;
1041 }
1042 let scraperService;
1043 if (def.scraper.service) {
1044 scraperService = await loadScraperService(root);
1045 } else if (def.scraper.static) {
1046 scraperService = useScraper().staticService;
1047 }
1048 proxyResHooks.push((json, req, res) => {
1049 const api = useAPI(req);
1050 scraperService(json, api);
1051 });
1052 }
1053 if (def.proxy) {
1054 if (def.proxy.responseRewriters) {
1055 def.proxy.selfHandleResponse = true;
1056 def.proxy.responseRewriters.forEach((rewriterName) => {
1057 proxyResHooks.push(internalMiddlewares[rewriterName]);
1058 });
1059 }
1060 proxies.push({
1061 path: path.join("/"),
1062 context: {
1063 ...getProxy(def),
1064 changeOrigin: true,
1065 selfHandleResponse: Boolean(proxyResHooks.length),
1066 pathRewrite: {
1067 [path.join("/")]: "/"
1068 },
1069 onProxyReq,
1070 onProxyRes: applyProxyRes(proxyResHooks, def)
1071 },
1072 def
1073 });
1074 }
1075 }
1076 return inheritance;
1077};
1078const setRoute = async (app, router, def, verb, root, inheritsProxy) => {
1079 const path = `${state$3.get("basePath")}/${root.join("/")}`;
1080 if (Object.keys(def.throttle).length) {
1081 app.use(path, getThrottleMiddleware(def));
1082 }
1083 const handler = async (event) => {
1084 let response;
1085 let applyTemplate = true;
1086 let staticExtension = "json";
1087 if (def.service) {
1088 const api = useAPI(event);
1089 const { serviceFile, service } = await loadService(root, verb);
1090 try {
1091 response = await service(api);
1092 } catch (e) {
1093 console.log("Error in service", serviceFile, e);
1094 throw e;
1095 }
1096 }
1097 if (def.static) {
1098 try {
1099 const params = getRouterParams(event);
1100 const query = getQuery(event);
1101 const { extensions } = def;
1102 const [result, extension] = await loadStatic({
1103 routePath: root,
1104 params,
1105 verb,
1106 query,
1107 extensions
1108 });
1109 response = result;
1110 staticExtension = extension;
1111 if (!response) {
1112 const [result2, extension2] = await loadScraped({
1113 routePath: root,
1114 params,
1115 verb,
1116 query,
1117 extensions
1118 });
1119 applyTemplate = false;
1120 response = result2;
1121 staticExtension = extension2;
1122 if (!response) {
1123 applyTemplate = true;
1124 response = {
1125 drosse: `loadStatic: file not found with routePath = ${root.join(
1126 "/"
1127 )}`
1128 };
1129 }
1130 }
1131 } catch (e) {
1132 response = {
1133 drosse: e.message,
1134 stack: e.stack
1135 };
1136 }
1137 }
1138 if (def.body) {
1139 response = def.body;
1140 applyTemplate = true;
1141 }
1142 if (applyTemplate && def.responseType !== "file" && staticExtension === "json" && def.template && Object.keys(def.template).length) {
1143 response = templates.list()[def.template](response);
1144 }
1145 if (def.responseType === "file" || staticExtension && staticExtension !== "json") {
1146 const content = await readFile(response);
1147 send(event, content, "application/octet-stream");
1148 }
1149 return response;
1150 };
1151 if (inheritsProxy) {
1152 app.use(path, eventHandler(handler), { match: (url) => url === "/" });
1153 } else {
1154 router[verb](path, eventHandler(handler));
1155 }
1156 logger.success(
1157 `-> ${verb.toUpperCase().padEnd(7)} ${state$3.get("basePath")}/${root.join(
1158 "/"
1159 )}`
1160 );
1161};
1162const createAssets = ({ app, assets }) => {
1163 if (!assets) {
1164 return;
1165 }
1166 const _assets = [];
1167 assets.forEach(({ path: routePath, context }) => {
1168 const fsPath = join(state$3.get("root"), context.target);
1169 let mwPath = routePath;
1170 if (routePath.includes("*")) {
1171 mwPath = mwPath.substring(0, mwPath.indexOf("*"));
1172 mwPath = mwPath.substring(0, mwPath.lastIndexOf("/"));
1173 const re = new RegExp(routePath.replaceAll("*", "[^/]*"));
1174 app.use(
1175 mwPath,
1176 eventHandler((event) => {
1177 if (re.test(`${mwPath}${event.node.req.url}`)) {
1178 setResponseHeader(event, "x-wildcard-asset-target", context.target);
1179 }
1180 })
1181 );
1182 _assets.push({ mwPath, fsPath, wildcardPath: routePath });
1183 } else {
1184 _assets.push({ mwPath, fsPath });
1185 }
1186 });
1187 _assets.forEach(({ mwPath, fsPath, wildcardPath }) => {
1188 app.use(
1189 mwPath,
1190 eventHandler(async (event) => {
1191 const wildcardAssetTarget = getResponseHeader(
1192 event,
1193 "x-wildcard-asset-target"
1194 );
1195 if (wildcardAssetTarget) {
1196 event.node.req.url = wildcardAssetTarget.replace(
1197 join(state$3.get("assetsPath"), mwPath),
1198 ""
1199 );
1200 }
1201 return fromNodeMiddleware(serveStatic$1(fsPath))(event);
1202 })
1203 );
1204 logger.info(
1205 `-> STATIC ASSETS ${wildcardPath || mwPath || "/"} => ${fsPath}`
1206 );
1207 });
1208};
1209const createProxies = ({ app, router, proxies }) => {
1210 if (!proxies) {
1211 return;
1212 }
1213 proxies.forEach(({ path, context, def }) => {
1214 const proxyMw = createProxyMiddleware({ ...context, logLevel: "warn" });
1215 if (Object.keys(def.throttle || {}).length) {
1216 app.use(path || "/", getThrottleMiddleware(def));
1217 }
1218 app.use(
1219 path || "/",
1220 eventHandler((event) => {
1221 return new Promise((resolve, reject) => {
1222 const next = (err) => {
1223 if (err) {
1224 reject(err);
1225 } else {
1226 resolve(true);
1227 }
1228 };
1229 setResponseHeader(event, "x-proxied", true);
1230 return proxyMw(event.node.req, event.node.res, next);
1231 });
1232 })
1233 );
1234 logger.info(`-> PROXY ${path || "/"} => ${context.target}`);
1235 });
1236};
1237
1238let state$2 = config.commands;
1239function useCommand() {
1240 return {
1241 merge(commands) {
1242 state$2 = { ...state$2, ...commands };
1243 },
1244 executeCommand(command) {
1245 return get(state$2, command.name)(command.params);
1246 }
1247 };
1248}
1249
1250let state$1 = [];
1251function useMiddlewares() {
1252 return {
1253 append(mw) {
1254 state$1 = [...state$1, ...mw];
1255 },
1256 set(mw) {
1257 state$1 = [...mw];
1258 },
1259 list() {
1260 return state$1;
1261 }
1262 };
1263}
1264
1265const api = useAPI();
1266const { executeCommand } = useCommand();
1267const { loadDb } = useDB();
1268const { checkRoutesFile, loadUuid, getUserConfig, getRoutesDef } = useIO();
1269const { isEsmMode } = useIO$1();
1270const middlewares = useMiddlewares();
1271const state = useState();
1272let app, emit$1, root, listener, userConfig, version, port;
1273const init = async (_root, _emit, _version, _port) => {
1274 version = _version;
1275 root = resolve(_root);
1276 emit$1 = _emit;
1277 port = _port;
1278 state.set("root", root);
1279 userConfig = await getUserConfig();
1280 state.merge(userConfig);
1281 if (!await checkRoutesFile()) {
1282 logger.error(
1283 `Please create a "${state.get(
1284 "routesFile"
1285 )}.json" file in this directory: ${state.get("root")}, and restart.`
1286 );
1287 process.exit();
1288 }
1289 await loadUuid();
1290};
1291const initServer = async () => {
1292 app = createApp({ debug: true });
1293 await loadDb();
1294 middlewares.set(config.middlewares);
1295 if (userConfig.middlewares) {
1296 middlewares.append(userConfig.middlewares);
1297 }
1298 if (userConfig.templates) {
1299 useTemplates().merge(userConfig.templates);
1300 }
1301 if (userConfig.commands) {
1302 useCommand().merge(userConfig.commands(api));
1303 }
1304 logger.info("-> Middlewares:");
1305 console.info(middlewares.list());
1306 for (let mw of middlewares.list()) {
1307 if (typeof mw !== "function") {
1308 mw = internalMiddlewares[mw];
1309 }
1310 if (mw.length === 2) {
1311 mw = curry(mw)(api);
1312 }
1313 app.use(eventHandler(mw));
1314 }
1315 const routesDef = await getRoutesDef();
1316 const router = createRouter();
1317 await createRoutes(app, router, routesDef);
1318 app.use(
1319 eventHandler((req) => {
1320 if (!Object.values(state.get("reservedRoutes")).includes(req.url)) {
1321 emit$1("request", {
1322 url: req.url,
1323 method: req.method
1324 });
1325 }
1326 })
1327 );
1328 app.use(
1329 state.get("reservedRoutes").ui,
1330 eventHandler(internalMiddlewares["open-cors"])
1331 );
1332 router.get(
1333 state.get("reservedRoutes").ui,
1334 eventHandler(() => {
1335 return { routes: routesDef };
1336 })
1337 );
1338 app.use(
1339 state.get("reservedRoutes").cmd,
1340 eventHandler(internalMiddlewares["open-cors"])
1341 );
1342 router.post(
1343 state.get("reservedRoutes").cmd,
1344 eventHandler(async (event) => {
1345 const body = await readBody(event);
1346 if (body.cmd === "restart") {
1347 emit$1("restart");
1348 if (isEsmMode()) {
1349 return {
1350 restarted: false,
1351 comment: RESTART_DISABLED_IN_ESM_MODE
1352 };
1353 } else {
1354 return { restarted: true };
1355 }
1356 } else {
1357 const result = await executeCommand({
1358 name: body.cmd,
1359 params: body
1360 });
1361 return result;
1362 }
1363 })
1364 );
1365 app.use(router);
1366};
1367const getDescription = () => ({
1368 isDrosse: true,
1369 version,
1370 uuid: state.get("uuid"),
1371 name: state.get("name"),
1372 proto: "http",
1373 port: state.get("port"),
1374 root: state.get("root"),
1375 routesFile: state.get("routesFile"),
1376 collectionsPath: state.get("collectionsPath")
1377});
1378const start = async () => {
1379 await initServer();
1380 const description = getDescription();
1381 console.log();
1382 logger.debug(
1383 `App ${description.name ? ansiColors.magenta(description.name) + " " : ""}(version ${ansiColors.magenta(version)}) running at:`
1384 );
1385 listener = await listen(toNodeListener(app), {
1386 port: port || description.port
1387 });
1388 if (typeof userConfig.extendServer === "function") {
1389 userConfig.extendServer({ server: listener.server, app, db: api.db });
1390 }
1391 if (typeof userConfig.onHttpUpgrade === "function") {
1392 listener.server.on("upgrade", userConfig.onHttpUpgrade);
1393 }
1394 console.log();
1395 logger.debug(`Mocks root: ${ansiColors.magenta(description.root)}`);
1396 console.log();
1397 emit$1("start", state.get());
1398 return listener;
1399};
1400const stop = async () => {
1401 await listener.close();
1402 logger.warn("Server stopped");
1403 emit$1("stop");
1404};
1405const restart = async () => {
1406 if (isEsmMode()) {
1407 console.warn(RESTART_DISABLED_IN_ESM_MODE);
1408 console.info("Please use ctrl+c to restart drosse.");
1409 } else {
1410 await stop();
1411 await start();
1412 }
1413};
1414const describe = () => {
1415 return getDescription();
1416};
1417
1418function serveStatic(root, port, proxy) {
1419 const app = createApp({ debug: true });
1420 const staticMw = serveStatic$1(root, { fallthrough: false, redirect: false });
1421 if (proxy) {
1422 const proxyMw = createProxyMiddleware({
1423 target: proxy,
1424 changeOriging: true
1425 });
1426 app.use(async (req, res) => {
1427 let fileExists;
1428 try {
1429 await promises.access(join(root, req.url));
1430 fileExists = true;
1431 } catch {
1432 fileExists = false;
1433 }
1434 return new Promise((resolve, reject) => {
1435 const next = (err) => {
1436 if (err) {
1437 reject(err);
1438 } else {
1439 resolve(true);
1440 }
1441 };
1442 return fileExists ? staticMw(req, res, next) : proxyMw(req, res, next);
1443 });
1444 });
1445 } else {
1446 app.use("/", fromNodeMiddleware(staticMw));
1447 }
1448 listen(app, { port });
1449}
1450
1451function db(vorpal, { config, restart }) {
1452 const dropDatabase = async () => {
1453 const dbFile = join(config.root, config.database);
1454 await promises.rm(dbFile);
1455 return restart();
1456 };
1457 vorpal.command("db drop", "Delete the database file.").action(dropDatabase);
1458}
1459
1460function cli(vorpal, params) {
1461 vorpal.command("rs", "Restart the server.").action(() => {
1462 return params.restart();
1463 });
1464 db(vorpal, params);
1465}
1466
1467function useCLI(config, restart) {
1468 const vorpal = _vorpal();
1469 const { executeCommand } = useCommand();
1470 const runCommand = async (name, params) => executeCommand({ name, params });
1471 return {
1472 extend(callback) {
1473 callback(vorpal, { config, runCommand, restart });
1474 },
1475 start() {
1476 this.extend(cli);
1477 vorpal.delimiter("\u{1F3A4}").show();
1478 }
1479 };
1480}
1481
1482const defineDrosseServer = (userConfig) => userConfig;
1483const defineDrosseScraper = (handler) => handler;
1484const defineDrosseService = (handler) => handler;
1485process.title = `node drosse ${process.argv[1]}`;
1486let _version, discover, description, noRepl;
1487const getVersion = async () => {
1488 if (!_version) {
1489 try {
1490 const importPath = import.meta.url.replace("file://", "/");
1491 const packageFile = join(importPath, "..", "..", "package.json");
1492 const content = await readFile(packageFile, "utf8");
1493 _version = JSON.parse(content).version;
1494 } catch (e) {
1495 console.error("Failed to get Drosse version", e);
1496 }
1497 }
1498 return _version;
1499};
1500const emit = async (event, data) => {
1501 switch (event) {
1502 case "start":
1503 description = describe();
1504 if (!Boolean(discover)) {
1505 discover = new Discover();
1506 discover.advertise(description);
1507 }
1508 try {
1509 setTimeout(() => {
1510 discover?.send(event, { uuid: description.uuid });
1511 }, 10);
1512 } catch (e) {
1513 console.error(e);
1514 }
1515 if (Boolean(noRepl)) {
1516 return;
1517 }
1518 const io = useIO();
1519 const cli = useCLI(data, restart);
1520 const userConfig = await io.getUserConfig(data.root);
1521 if (userConfig.cli) {
1522 cli.extend(userConfig.cli);
1523 }
1524 cli.start();
1525 break;
1526 case "restart":
1527 restart();
1528 break;
1529 case "stop":
1530 discover?.send(event, { uuid: description.uuid });
1531 break;
1532 case "request":
1533 discover?.send(event, { uuid: description.uuid, ...data });
1534 break;
1535 }
1536};
1537function getMatchablePath(path) {
1538 let stop = false;
1539 return path.replace(/^(.*\/\/)?\/(.*)$/g, "/$2").split("/").reduce((matchablePath, dir) => {
1540 if (["node_modules", "dist", "src"].includes(dir)) {
1541 stop = true;
1542 }
1543 if (!stop) {
1544 matchablePath.push(dir);
1545 }
1546 return matchablePath;
1547 }, []).join("/");
1548}
1549if (getMatchablePath(import.meta.url) === getMatchablePath(process.argv[1])) {
1550 yargs(hideBin(process.argv)).usage("Usage: $0 <cmd> [args]").command({
1551 command: "describe <rootPath>",
1552 desc: "Describe the mock server",
1553 handler: async (argv) => {
1554 const version = await getVersion();
1555 await init(argv.rootPath, emit, version);
1556 console.log(describe());
1557 process.exit();
1558 }
1559 }).command({
1560 command: "serve <rootPath>",
1561 desc: "Run the mock server",
1562 builder: {
1563 port: {
1564 alias: "p",
1565 describe: "HTTP port",
1566 type: "number"
1567 },
1568 norepl: {
1569 default: false,
1570 describe: "Disable repl mode",
1571 type: "boolean"
1572 }
1573 },
1574 handler: async (argv) => {
1575 noRepl = argv.norepl;
1576 const version = await getVersion();
1577 await init(argv.rootPath, emit, version, argv.port);
1578 return start();
1579 }
1580 }).command({
1581 command: "static <rootPath>",
1582 desc: "Run a static file server",
1583 builder: {
1584 port: {
1585 alias: "p",
1586 describe: "HTTP port",
1587 type: "number"
1588 },
1589 proxy: {
1590 alias: "P",
1591 describe: "Proxy requests to another host",
1592 type: "string"
1593 }
1594 },
1595 handler: async (argv) => {
1596 return serveStatic(argv.rootPath, argv.port, argv.proxy);
1597 }
1598 }).demandCommand(1).strict().parse();
1599}
1600
1601export { defineDrosseScraper, defineDrosseServer, defineDrosseService };