// load configs and spaces const config = require("./config.json"); const spaces = require("./spaces.json"); // load modules const { setInterval } = require("node:timers/promises"); const fetch = require("node-fetch"); const express = require("express"); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const NodeCache = require("node-cache"); const jp = require('jsonpath'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const cors = require('cors'); // set up things const cache = new NodeCache({ stdTTL: config.checkperiod * 3 }); let schema = buildSchema(` type Space { name: String id: String! open: Boolean! updatedAt: String! lastChange: String area: String } type Query { isOpen(id: String): Boolean spaces: [Space!]! inArea(area: String): [Space!]! } `); let root = { isOpen: async ({ id }) => { let data = await prisma.space.findUnique({ where: { id: id } }); return data.open; }, spaces: async () => { let data = await prisma.space.findMany(); return data; }, inArea: async ({ area }) => { // try to find the exact area first, if that fails, try to find at least a partial match let data = await prisma.space.findMany({ where: { area: area } }); if (data.length == 0) { data = await prisma.space.findMany({ where: { area: { contains: area } } }); } return data; } }; let app = express(); app.use(cors()); app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.listen(config.port || 4000); console.log(`Running a GraphQL API server at localhost:${config.port || 4000}/graphql`); // CHECK LOOP (async function () { await loop(); for await (const time of setInterval(config.checkperiod * 1000)) { await loop(); } })(); async function loop() { console.log(new Date(), "Checking for spaces..."); let changecount = 0; for (const space of spaces) { let response = await checkSpace(space); if (typeof response.open === "undefined") { console.error(`The space ${space.id} might not be reachable. Please check the endpoint.`); continue; } if (response.open != cache.get(space.id)) { // update or create the space in the database let update = await prisma.space.upsert({ where: { id: space.id }, update: { open: response.open, lastChange: response.lastchange }, create: { id: space.id, open: response.open, lastChange: response.lastchange, name: space.name, area: space.area, } }); changecount++; console.debug(`Space ${space.id} changed to ${response.open ? "open" : "closed"}.`); } cache.set(space.id, response.open); } console.log(new Date(), "Check complete, updated", changecount, "spaces."); } // HELPER FUNCTIONS async function checkSpace(space) { let response, data, open; let lastchange = null; try { response = await fetch(space.endpoint); data = await response.json(); } catch (e) { console.error(`The space ${space.id} might not be reachable. Please check the endpoint. Error: ${e}`); } // Check if the space is using the SpaceAPI standard if (!space.path) { try { open = data.state.open; lastchange = new Date(data.state.lastchange*1000); } catch { console.error(`The space ${space.id} is not using the SpaceAPI standard. Please specify a path.`); } } else { try { // Query the JSONPath. If the expected value is not set, assume true or a similar value like 1. open = (jp.query(data, space.path) == (space.expected ? space.expected : true)); } catch { console.error(`The space ${space.id} has an invalid JSONPath to the target value. Please use https://jsonpath.com/ to evaluate the path.`); } } return { open, lastchange }; }