├── .gitignore ├── Architecture.png ├── README.md ├── backend ├── .dockerignore ├── .eslintrc.json ├── Dockerfile ├── package-lock.json ├── package.json ├── src │ ├── MaterializeClient │ │ ├── TailStream │ │ │ └── index.ts │ │ ├── TransformStream │ │ │ └── index.ts │ │ ├── WriteStream │ │ │ └── index.ts │ │ └── index.ts │ └── app.ts └── tsconfig.json ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── ColorModeSwitcher.tsx │ ├── Logo.tsx │ ├── components │ │ └── AntennasMap │ │ │ ├── ButtonSelection │ │ │ └── index.js │ │ │ ├── DotClone │ │ │ └── index.js │ │ │ └── index.tsx │ ├── index.tsx │ ├── link.ts │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── serviceWorker.ts │ ├── setupTests.ts │ ├── test-utils.tsx │ └── theme.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── helper ├── .dockerignore ├── .eslintrc.json ├── Dockerfile ├── package-lock.json ├── package.json ├── src │ └── app.ts └── tsconfig.json ├── microservice ├── .dockerignore ├── .eslintrc.json ├── Dockerfile ├── package-lock.json ├── package.json ├── src │ └── app.ts └── tsconfig.json └── postgres ├── Dockerfile ├── create.sh ├── create.sql ├── rollback.sql ├── seed.sh └── seed.sql /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joacoc/antennas-manhattan/9626ff0e82caed24f93724f4b2ff70623ab22a90/Architecture.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manhattan Antennas Performance 2 | 3 | https://github.com/joacoc/antennas-manhattan/assets/11491779/38d5a50b-b6e5-4596-8b0c-c6a4e0cd08bd 4 | 5 | If you want to try it right now, clone the project and run: 6 | 7 | ``` 8 | docker-compose up 9 | ``` 10 | 11 | After a successful build: 12 | 13 | ``` 14 | # Check in your browser 15 | localhost:3000 16 | 17 | # Alternatively connect to: 18 | # Materialize 19 | psql postgresql://materialize:materialize@localhost:6875/materialize 20 | 21 | # Postgres 22 | psql postgresql://postgres:pg_password@localhost:5432/postgres 23 | ``` 24 | 25 | --- 26 | 27 | ## Let’s begin. 28 | 29 | An infrastructure working safe and healthy is critical. We, developers, know this very well. In other businesses, like in software, there are vital infrastructures, such as mobile antennas (4G, 5G) in telecommunications companies.
30 | If there is some issue, it needs to be detected and fixed quickly; otherwise, customers will complain, or even worse, move to the competition (churn rate is serious business). 31 | 32 | Antennas manufacturers share [key performance indicators](https://www.ericsson.com/en/reports-and-papers/white-papers/performance-verification-for-5g-nr-deployments) with their telecommunications companies clients. Let's call all these indicators "performance". Rather than setting a 5G antenna manually to provide indicators, let randomness generate this value, providing even more excitement and entertainment to the case than in real life. 33 | 34 | Each antenna has a fixed range where is capable of serving clients. In a map, a green, yellow, or red (healthy, semi-healthy, and unhealthy) circle will denote this area. 35 | 36 | If the last-half-minute average performance is greater than 5, the antenna is healthy.
37 | If it is greater than 4.75 but less than 5, it is semi-healthy.
38 | If it is less than 4.75, the antenna is unhealthy.
39 | 40 | In case an antenna is unhealthy beyond a period of seconds, a whole set of helper antennas will be deployed to improve the performance in the area. After a few seconds of improvement they will be deactivated. 41 | 42 | All this information needs to be processed, analyzed, and served, and that's where Materialize will do the work for us efficiently. 43 | 44 | ## Detailes steps 45 | 46 | There are different ways to achieve a result like this one using Materialize, but for this case, the following strategy fulfill our needs: 47 | 48 | 1. Postgres, where all the base data resides. 49 | 2. Materialize to process and serve the antenna's performance. 50 | 3. Helper process to generate the antennas random data and initialize Materialize 51 | 4. Node.js GraphQL API connects to Materialize using [tails](https://materialize.com/docs/sql/tail/#conceptual-framework). 52 | 5. React front-end displaying the information using GraphQL subscriptions. 53 | 6. Microservice deploying and pushing helper antennas when performance is low 54 | 55 | _Our source, Postgres, could be alternatively replaced with any other [Materialize source](https://materialize.com/docs/sql/create-source/#conceptual-framework)_ 56 | 57 | ![Architecture](https://user-images.githubusercontent.com/11491779/155920578-7984244a-6382-4628-a87b-00e1f6ad1acd.png) 58 | 59 |
60 | 61 | 1. To begin with, Postgres needs to be up and running. You can reuse this [custom image with SQLs and shell scripts](https://github.com/MaterializeInc/developer-experience/tree/main/mz-playground/postgres-graphql/postgres) that will get executed in [Postgres initialization](https://github.com/docker-library/docs/blob/master/postgres/README.md#initialization-scripts).

The scripts creates the schemas and defines everything we need to use them as a source: 62 | 63 | ```sql 64 | -- Antennas table will contain the identifier and geojson for each antenna. 65 | CREATE TABLE antennas ( 66 | antenna_id INT GENERATED ALWAYS AS IDENTITY, 67 | geojson JSON NOT NULL 68 | ); 69 | 70 | 71 | -- Antennas performance table will contain every performance update available 72 | CREATE TABLE antennas_performance ( 73 | antenna_id INT, 74 | clients_connected INT NOT NULL, 75 | performance INT NOT NULL, 76 | updated_at timestamp NOT NULL 77 | ); 78 | 79 | 80 | -- Enable REPLICA for both tables 81 | ALTER TABLE antennas REPLICA IDENTITY FULL; 82 | ALTER TABLE antennas_performance REPLICA IDENTITY FULL; 83 | 84 | 85 | -- Create publication on the created tables 86 | CREATE PUBLICATION antennas_publication_source FOR TABLE antennas, antennas_performance; 87 | 88 | 89 | -- Create user and role to be used by Materialize 90 | CREATE ROLE materialize REPLICATION LOGIN PASSWORD 'materialize'; 91 | GRANT SELECT ON antennas, antennas_performance TO materialize; 92 | ``` 93 | 94 |
95 | 96 | 2-3. Once Postgres is up and running, Materialize will be ready to consume it. If you are automating a deployment, a [helper process](https://github.com/MaterializeInc/developer-experience/blob/main/mz-playground/postgres-graphql/helper/src/app.ts) can do the job to set up sources and views in Materialize and also feed Postgres indefinitely with data.

The SQL script to build Materialize schema is the next one: 97 | 98 | ```sql 99 | -- All these queries run inside the helper process. 100 | 101 | 102 | -- Create the Postgres Source 103 | CREATE MATERIALIZED SOURCE IF NOT EXISTS antennas_publication_source 104 | FROM POSTGRES 105 | CONNECTION 'host=postgres port=5432 user=materialize password=materialize dbname=postgres' 106 | PUBLICATION 'antennas_publication_source'; 107 | 108 | 109 | -- Turn the Postgres tables into Materialized Views 110 | CREATE MATERIALIZED VIEWS FROM SOURCE antennas_publication_source; 111 | 112 | 113 | -- Filter last half minute updates 114 | CREATE MATERIALIZED VIEW IF NOT EXISTS last_half_minute_updates AS 115 | SELECT A.antenna_id, A.geojson, performance, AP.updated_at, ((CAST(EXTRACT( epoch from AP.updated_at) AS NUMERIC) * 1000) + 30000) 116 | FROM antennas A JOIN antennas_performance AP ON (A.antenna_id = AP.antenna_id) 117 | WHERE ((CAST(EXTRACT( epoch from AP.updated_at) AS NUMERIC) * 1000) + 30000) > mz_logical_timestamp(); 118 | 119 | 120 | -- Aggregate by anntena ID and GeoJSON to obtain the average performance in the last half minute. 121 | CREATE MATERIALIZED VIEW IF NOT EXISTS last_half_minute_performance_per_antenna AS 122 | SELECT antenna_id, geojson, AVG(performance) as performance 123 | FROM last_half_minute_updates 124 | GROUP BY antenna_id, geojson; 125 | ``` 126 | 127 | Antennas data generation statement: 128 | 129 | ```sql 130 | -- Insert data using the helper process. 131 | INSERT INTO antennas_performance (antenna_id, clients_connected, performance, updated_at) VALUES ( 132 | ${antennaId}, 133 | ${Math.ceil(Math.random() * 100)}, 134 | ${Math.random() * 10}, 135 | now() 136 | ); 137 | ``` 138 | 139 | 4. Now, the information should be ready to consume.

140 | The back-end works with [Graphql-ws](https://github.com/enisdenjo/graphql-ws). Subscriptions and tails go together like Bonnie and Clyde. Multiple applications send ongoing events to the front-end with sockets or server-sent events (SSE), becoming super handy to use with `tails`. Rather than constantly sending queries back-and-forth, we can run a single `tail last_half_minute_performance_per_antenna with (snapshot)` and send the results more efficiently.

141 | The back-end will use a modified client to run these tails. It implements internally [Node.js stream interfaces](https://nodejs.org/api/stream.html) to handle [backpressure](https://github.com/MaterializeInc/developer-experience/blob/main/mz-playground/postgres-graphql/backend/src/MaterializeClient/TailStream/index.ts), create one second batches and group all the changes in one map [(summary)](https://github.com/MaterializeInc/developer-experience/blob/main/mz-playground/postgres-graphql/backend/src/MaterializeClient/TransformStream/index.ts). 142 | 143 | 5. The front-end doesn't require going deep since it will consist of only one component. Apollo GraphQL subscribes to our back-end, and the antennas information gets displayed in a list and a visual map. The frequency at which the information updates is every one second. 144 | 145 | 6. The microservice behaves similar to the front-end. Rather than connecting directly to Materialize, it will subscribe to the GraphQL API and subscribe to the antenna's performance. Once a low performance has been detected multiple times a set of helper antennas will be deployed. 146 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | 13 | # Bundle app source 14 | COPY . . 15 | 16 | EXPOSE 4000 17 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "start": "tsc && node dist/app.js", 8 | "lint": "eslint . --ext .ts", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^5.10.1", 16 | "@typescript-eslint/parser": "^5.10.1", 17 | "eslint": "^8.7.0", 18 | "typescript": "^4.5.5" 19 | }, 20 | "dependencies": { 21 | "@types/pg": "^8.6.4", 22 | "apollo-server-express": "^3.6.2", 23 | "express": "^4.17.3", 24 | "graphql": "^16.3.0", 25 | "graphql-ws": "^5.5.5", 26 | "pg": "^8.7.3", 27 | "ws": "^8.4.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/MaterializeClient/TailStream/index.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from "stream"; 2 | import { Client } from "pg"; 3 | 4 | /** 5 | * Thanks to Petros Angelatos 6 | * https://gist.github.com/petrosagg/804e5f009dee1cb8af688654ba396258 7 | * This class reads from a cursor in PostgreSQL 8 | */ 9 | export default class TailStream extends Readable { 10 | client: Client; 11 | 12 | cursorId: string; 13 | 14 | pendingReads: number; 15 | 16 | currentRows: Array; 17 | 18 | BreakException = {}; 19 | 20 | intervalId: NodeJS.Timer; 21 | 22 | runningQuery: boolean; 23 | 24 | constructor(client: Client, cursorId: string) { 25 | super({ 26 | highWaterMark: 1000, 27 | objectMode: true, 28 | }); 29 | this.client = client; 30 | this.cursorId = cursorId; 31 | this.pendingReads = 0; 32 | this.runningQuery = false; 33 | } 34 | 35 | /** 36 | * Readable method to fetch tail data 37 | * @param n 38 | */ 39 | _read(n: number): void { 40 | if (this.pendingReads <= 0) { 41 | this.client 42 | .query(`FETCH ${n} ${this.cursorId} WITH (TIMEOUT='1s');`) 43 | .then(({ rows, rowCount }) => { 44 | if (rowCount === 0) { 45 | console.log("Empty results from tail. Staring interval read."); 46 | /** 47 | * Wait for data from the tail 48 | */ 49 | this.intervalId = setInterval(() => this.intervalRead(n), 500); 50 | } else { 51 | /** 52 | * Process data 53 | */ 54 | this.process(rows); 55 | } 56 | }) 57 | .catch(this.catchClientErr); 58 | } else { 59 | /** 60 | * Process any additional rows 61 | */ 62 | this.currentRows = this.currentRows.slice( 63 | this.currentRows.length - this.pendingReads, 64 | this.currentRows.length 65 | ); 66 | try { 67 | this.currentRows.forEach((row) => { 68 | this.pendingReads -= 1; 69 | const backPressure = !this.push(row); 70 | if (backPressure) { 71 | throw this.BreakException; 72 | } 73 | }); 74 | } catch (e) { 75 | if (e !== this.BreakException) throw e; 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Capture any error while fetching tail results 82 | * @param clientReasonErr 83 | */ 84 | catchClientErr(clientReasonErr: any) { 85 | console.error("Error querying this cursor."); 86 | console.error(clientReasonErr); 87 | 88 | if (this.intervalId) { 89 | clearInterval(this.intervalId); 90 | } 91 | 92 | this.destroy(clientReasonErr); 93 | } 94 | 95 | /** 96 | * Process and push rows 97 | * @param rows 98 | */ 99 | process(rows: Array): void { 100 | try { 101 | rows.forEach((row) => { 102 | this.pendingReads -= 1; 103 | const backPressure = !this.push(row); 104 | if (backPressure) { 105 | console.log("Oops. Backpressure."); 106 | throw this.BreakException; 107 | } 108 | }); 109 | } catch (e) { 110 | if (e !== this.BreakException) throw e; 111 | } 112 | } 113 | 114 | /** 115 | * Interval fetching used when there are no results from the TAIL 116 | * Rather than pausing and waiting for results 117 | * Run a tail fetch every 500ms. 118 | * This is needed because if there is no update from the tail the pipe will close. 119 | * Another alternative is to send dummy data but this could end up filtering data all the time. 120 | * Another alternative is to push whenever is available rather than "poll" but how backpressure is handled? 121 | * @param n 122 | */ 123 | intervalRead(n: number): void { 124 | if (this.runningQuery === false) { 125 | if (this.destroyed) { 126 | clearInterval(this.intervalId); 127 | return; 128 | } 129 | 130 | this.runningQuery = true; 131 | this.client 132 | .query(`FETCH ${n} ${this.cursorId} WITH (TIMEOUT='1s');`) 133 | .then(({ rows, rowCount }) => { 134 | if (rowCount > 0) { 135 | this.process(rows); 136 | clearInterval(this.intervalId); 137 | console.log("New results from the tail. Finishing interval read."); 138 | } else { 139 | console.log("Nothing from interval read."); 140 | } 141 | }) 142 | .catch(this.catchClientErr) 143 | .finally(() => { 144 | this.runningQuery = false; 145 | }); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /backend/src/MaterializeClient/TransformStream/index.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from "stream"; 2 | 3 | interface Antenna { 4 | antenna_id: string; 5 | geojson: string; 6 | performance: number; 7 | } 8 | 9 | /** 10 | * This class creates a batch of chunks. In this way every chunk is not a row but an array of rows. 11 | * This will improve the performance of the writing. 12 | * A timeout is needed in case the batch length is lower than the highwatermark for a long period of time. 13 | */ 14 | export default class TransformStream extends Transform { 15 | batch = new Array(); 16 | 17 | size: number; 18 | 19 | constructor() { 20 | super({ 21 | highWaterMark: 100, 22 | objectMode: true, 23 | }); 24 | 25 | this.cleanBatch(); 26 | } 27 | 28 | cleanBatch() { 29 | this.batch = new Array(); 30 | } 31 | 32 | _transform(row: any, encoding: string, callback: () => void) { 33 | const { mz_progressed: mzProgressed } = row; 34 | 35 | if (mzProgressed) { 36 | this.push(this.batch); 37 | this.cleanBatch(); 38 | } else { 39 | this.batch.push(row); 40 | } 41 | callback(); 42 | } 43 | 44 | _flush(callback: () => void) { 45 | if (this.batch.length) { 46 | this.push(this.batch); 47 | this.cleanBatch(); 48 | } 49 | callback(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/MaterializeClient/WriteStream/index.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "stream"; 2 | 3 | /** 4 | * This class is in charge of writing every chunk (array of rows) 5 | * into a redis instance to send to all the users. 6 | */ 7 | export default class WriteStream extends Writable { 8 | listener: (results: Array) => void; 9 | 10 | constructor(listener: (results: Array) => void) { 11 | super({ 12 | highWaterMark: 1000, 13 | objectMode: true, 14 | }); 15 | 16 | this.listener = listener; 17 | } 18 | 19 | _write( 20 | rows: Array, 21 | encoding: BufferEncoding, 22 | callback: (error?: Error) => void 23 | ): void { 24 | if (rows && rows.length > 0) { 25 | this.listener(rows); 26 | } 27 | 28 | callback(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/MaterializeClient/index.ts: -------------------------------------------------------------------------------- 1 | import { Pool, QueryResult, Client as PgClient, PoolConfig } from "pg"; 2 | import EventEmitter from "events"; 3 | import TailStream from "./TailStream"; 4 | import TransformStream from "./TransformStream"; 5 | import WriteStream from "./WriteStream"; 6 | 7 | /** 8 | * Custom Materialize Client created for Demo purposes 9 | */ 10 | export default class MaterializeClient { 11 | pool: Pool | undefined; 12 | config: PoolConfig; 13 | 14 | constructor(config: PoolConfig) { 15 | this.config = config; 16 | this.pool = new Pool(config); 17 | } 18 | 19 | /** 20 | * Run a query in Materialize 21 | * @param statement 22 | * @returns 23 | */ 24 | query(statement: string): Promise { 25 | const queryPromise = new Promise((res, rej) => { 26 | this.pool.connect(async (connectErr, poolClient, release) => { 27 | try { 28 | /** 29 | * Check errors in connection 30 | */ 31 | if (connectErr) { 32 | console.error(`Error connecting pool to run query.`); 33 | rej(connectErr); 34 | return; 35 | } 36 | 37 | /** 38 | * Control pool error listeners 39 | */ 40 | if (poolClient.listenerCount("error") === 0) { 41 | poolClient.on("error", (clientErr) => { 42 | console.error(`Client err: ${clientErr}`); 43 | rej(new Error("Error running query.")); 44 | }); 45 | } 46 | 47 | /** 48 | * Run query 49 | */ 50 | const response = await poolClient.query(statement); 51 | res(response); 52 | } catch (e) { 53 | console.error("Error running query."); 54 | console.error(e); 55 | rej(e); 56 | } finally { 57 | try { 58 | /** 59 | * After we stop/destroy a client release function dissapears. 60 | */ 61 | if (release) { 62 | release(); 63 | } 64 | } catch (e) { 65 | console.error(e); 66 | console.error("Error realeasing client."); 67 | } 68 | } 69 | }); 70 | }); 71 | 72 | return queryPromise; 73 | } 74 | 75 | /** 76 | * Run a tail in Materialize 77 | * @param statement 78 | * @param eventEmmiter 79 | * @returns 80 | */ 81 | async tail(statement: string, eventEmmiter: EventEmitter): Promise { 82 | return new Promise((res, rej) => { 83 | const asyncStream = async () => { 84 | /** 85 | * Create a single client per tail rather than re-using a pool's client 86 | */ 87 | const singleClient = new PgClient(this.config); 88 | 89 | /** 90 | * Client ending handler 91 | */ 92 | let clientEnded = false; 93 | const endClient = () => { 94 | console.log("Ending client."); 95 | if (clientEnded === false) { 96 | clientEnded = true; 97 | singleClient.end((err) => { 98 | if (err) { 99 | console.error("Error ending client."); 100 | console.debug(err); 101 | } 102 | }); 103 | } 104 | }; 105 | 106 | try { 107 | singleClient.on("error", (clientErr) => { 108 | console.error(`Client err: ${clientErr}`); 109 | rej(clientErr); 110 | }); 111 | 112 | singleClient.on("end", () => { 113 | console.log("Client end."); 114 | res(); 115 | }); 116 | 117 | await singleClient.connect(); 118 | await singleClient.query( 119 | `BEGIN; DECLARE mz_cursor CURSOR FOR ${statement} WITH (SNAPSHOT, PROGRESS);` 120 | ); 121 | 122 | /** 123 | * Listen to tail data updates 124 | */ 125 | const listener = (results: Array) => { 126 | eventEmmiter.emit("data", results); 127 | }; 128 | 129 | /** 130 | * Listen to tail errors 131 | */ 132 | const handleTailError = (err) => { 133 | console.error("Error inside tail: ", err); 134 | rej(err); 135 | }; 136 | 137 | const tailStream = new TailStream(singleClient, "mz_cursor"); 138 | tailStream.on("error", handleTailError); 139 | 140 | const transfromStream = new TransformStream(); 141 | const writeStream = new WriteStream(listener); 142 | 143 | /** 144 | * Listen to disconnects from the client 145 | */ 146 | eventEmmiter.on("disconnect", () => { 147 | if (tailStream.destroyed === false) { 148 | tailStream.destroy(); 149 | } 150 | endClient(); 151 | }); 152 | 153 | /** 154 | * Listen to pipe closing due to success or failure 155 | */ 156 | const streamPipe = tailStream.pipe(transfromStream).pipe(writeStream); 157 | streamPipe.on("close", () => { 158 | console.log("Pipe closed"); 159 | endClient(); 160 | res(); 161 | }); 162 | } catch (clientError) { 163 | endClient(); 164 | rej(clientError); 165 | } 166 | }; 167 | 168 | asyncStream().catch((asyncErr) => { 169 | console.error("Error inside async stream."); 170 | console.error(asyncErr); 171 | rej(asyncErr); 172 | }); 173 | }); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { WebSocketServer } from "ws"; 3 | import { Extra, useServer } from "graphql-ws/lib/use/ws"; 4 | import { buildSchema, parse, validate } from "graphql"; 5 | import MaterializeClient from "./MaterializeClient"; 6 | import EventEmitter from "events"; 7 | import { Pool } from "pg"; 8 | import { Context, SubscribeMessage } from "graphql-ws"; 9 | 10 | /** 11 | * Materialize Client 12 | */ 13 | const materializeClient = new MaterializeClient({ 14 | // host: "localhost", 15 | host: "materialized", 16 | port: 6875, 17 | user: "materialize", 18 | password: "materialize", 19 | database: "materialize", 20 | query_timeout: 5000, 21 | }); 22 | 23 | /** 24 | * Postgres Client 25 | */ 26 | const postgresPool = new Pool({ 27 | // host: "localhost", 28 | host: "postgres", 29 | port: 5432, 30 | user: "postgres", 31 | password: "pg_password", 32 | database: "postgres", 33 | }); 34 | 35 | /** 36 | * Build GraphQL Schema 37 | */ 38 | const schema = buildSchema(` 39 | type Antenna { 40 | antenna_id: String 41 | geojson: String 42 | performance: Float 43 | diff: Int 44 | timestamp: Float 45 | } 46 | 47 | type Query { 48 | getAntennas: [Antenna] 49 | } 50 | 51 | type Mutation { 52 | crashAntenna(antenna_id: String!): Antenna 53 | } 54 | 55 | type Subscription { 56 | antennasUpdates: [Antenna] 57 | } 58 | `); 59 | 60 | /** 61 | * Map to follow connections and tails 62 | */ 63 | const connectionEventEmitter = new EventEmitter(); 64 | 65 | /** 66 | * Build a custom Postgres insert with a low performance value to crash antenna 67 | * @param antennaId Antenna Identifier 68 | * @returns 69 | */ 70 | function buildQuery(antennaId: number) { 71 | return ` 72 | INSERT INTO antennas_performance (antenna_id, clients_connected, performance, updated_at) VALUES ( 73 | ${antennaId}, 74 | ${Math.ceil(Math.random() * 100)}, 75 | -100, 76 | now() 77 | ); 78 | `; 79 | } 80 | 81 | /** 82 | * Queries 83 | */ 84 | const getAntennas = async () => { 85 | try { 86 | const { rows } = await materializeClient.query("SELECT * FROM antennas;"); 87 | 88 | /** 89 | * Stringify GEOJson 90 | */ 91 | const mappedRows = rows.map((x) => ({ 92 | ...x, 93 | geojson: JSON.stringify(x.geojson), 94 | })); 95 | return mappedRows; 96 | } catch (err) { 97 | console.log("Error running query."); 98 | console.error(err); 99 | } 100 | 101 | return "Hello!"; 102 | }; 103 | 104 | /** 105 | * Mutations 106 | */ 107 | const crashAntenna = async (context) => { 108 | const { antenna_id: antennaId } = context; 109 | 110 | postgresPool.connect(async (err, client, done) => { 111 | if (err) { 112 | console.error(err); 113 | return; 114 | } 115 | 116 | try { 117 | /** 118 | * Smash the performance 119 | */ 120 | const query = buildQuery(antennaId); 121 | 122 | await client.query(query); 123 | } catch (clientErr) { 124 | console.error(clientErr); 125 | } finally { 126 | done(); 127 | } 128 | }); 129 | 130 | return { 131 | antenna_id: antennaId, 132 | }; 133 | }; 134 | 135 | /** 136 | * Subscriptions 137 | */ 138 | async function* antennasUpdates(_, ctxVars) { 139 | const [subscriptionId] = ctxVars; 140 | 141 | try { 142 | /** 143 | * Yield helpers 144 | */ 145 | let results = []; 146 | let resolve: (value: unknown) => void; 147 | let promise = new Promise((r) => (resolve = r)); 148 | let done = false; 149 | 150 | /** 151 | * Listen tail events 152 | */ 153 | const eventEmmiter = new EventEmitter(); 154 | eventEmmiter.on("data", (data) => { 155 | const mappedData: Array = data.map((x) => ({ 156 | ...x, 157 | geojson: JSON.stringify(x.geojson), 158 | diff: x.mz_diff, 159 | timestamp: x.mz_timestamp, 160 | })); 161 | results = mappedData; 162 | resolve(mappedData); 163 | promise = new Promise((r) => (resolve = r)); 164 | }); 165 | 166 | materializeClient 167 | .tail( 168 | "TAIL (SELECT * FROM last_half_minute_performance_per_antenna)", 169 | eventEmmiter 170 | ) 171 | .catch((tailErr) => { 172 | console.error("Error running tail."); 173 | console.error(tailErr); 174 | }) 175 | .finally(() => { 176 | console.log("Finished tail."); 177 | done = true; 178 | }); 179 | 180 | connectionEventEmitter.on("disconnect", (unsubscriptionId) => { 181 | if (subscriptionId === unsubscriptionId) { 182 | eventEmmiter.emit("disconnect"); 183 | done = true; 184 | } 185 | }); 186 | 187 | /** 188 | * Yield results 189 | */ 190 | while (!done) { 191 | await promise; 192 | yield { antennasUpdates: results }; 193 | results = []; 194 | } 195 | 196 | console.log("Outside done."); 197 | } catch (error) { 198 | console.error("Error running antennas updates subscription."); 199 | console.error(error); 200 | } 201 | } 202 | 203 | /** 204 | * The roots provide resolvers for each GraphQL operation 205 | */ 206 | const roots = { 207 | query: { 208 | getAntennas, 209 | }, 210 | mutation: { 211 | crashAntenna, 212 | }, 213 | subscription: { 214 | antennasUpdates, 215 | }, 216 | }; 217 | 218 | /** 219 | * Connection handlers 220 | */ 221 | const onClose = () => { 222 | console.log("onClose ids"); 223 | }; 224 | const onConnect = () => { 225 | console.log("onConnect."); 226 | }; 227 | const onDisconnect = (ctx) => { 228 | const ids = Object.keys(ctx.subscriptions); 229 | ids.forEach((id) => connectionEventEmitter.emit("disconnect", id)); 230 | }; 231 | const onError = (ctx, msg, errors) => { 232 | console.error("onError: ", ctx, msg, errors); 233 | }; 234 | const onSubscribe: ( 235 | ctx: Context>>, 236 | message: SubscribeMessage 237 | ) => any = (ctx, msg) => { 238 | const ids = Object.keys(ctx.subscriptions); 239 | console.log("OnSubscribe ids: ", ids); 240 | 241 | const args = { 242 | schema, 243 | operationName: msg.payload.operationName, 244 | document: parse(msg.payload.query), 245 | variableValues: msg.payload.variables, 246 | }; 247 | 248 | // dont forget to validate when returning custom execution args! 249 | const errors = validate(args.schema, args.document); 250 | if (errors.length > 0) { 251 | return errors; // return `GraphQLError[]` to send `ErrorMessage` and stop subscription 252 | } 253 | 254 | return { ...args, contextValue: ids }; 255 | }; 256 | 257 | /** 258 | * Setup server 259 | */ 260 | const app = express(); 261 | 262 | const server = app.listen(4000, () => { 263 | const wsServer = new WebSocketServer({ 264 | server, 265 | path: "/graphql", 266 | }); 267 | 268 | wsServer.on("error", (serverError) => { 269 | console.error("Server error: ", serverError); 270 | }); 271 | 272 | wsServer.on("connection", (ws) => { 273 | ws.on("error", (socketError) => { 274 | console.error("Socket error: ", socketError); 275 | }); 276 | }); 277 | 278 | useServer( 279 | { schema, roots, onClose, onDisconnect, onError, onSubscribe, onConnect }, 280 | wsServer 281 | ); 282 | 283 | console.log( 284 | "🚀 GraphQL web socket server listening on port 4000. \n\nUse 'ws://localhost:4000/graphql' to connect." 285 | ); 286 | }); 287 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "lib": ["esnext.asynciterable"] 10 | }, 11 | "lib": ["es2015"] 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | container_name: postgres 5 | build: 6 | context: ./postgres 7 | ports: 8 | - 5432:5432 9 | restart: always 10 | environment: 11 | POSTGRES_PASSWORD: pg_password 12 | command: 13 | - "postgres" 14 | - "-c" 15 | - "wal_level=logical" 16 | materialized: 17 | image: materialize/materialized:v0.20.0 18 | container_name: materialized 19 | restart: always 20 | ports: 21 | - 6875:6875 22 | depends_on: 23 | - postgres 24 | grafana_dashboard: 25 | image: materialize/dashboard 26 | container_name: grafana_dashboard 27 | restart: always 28 | ports: 29 | - 3001:3000 30 | depends_on: 31 | - materialized 32 | environment: 33 | MATERIALIZED_URL: materialized:6875 34 | helper: 35 | container_name: helper 36 | build: 37 | context: ./helper 38 | depends_on: 39 | - materialized 40 | backend: 41 | container_name: backend 42 | build: 43 | context: ./backend 44 | ports: 45 | - 4000:4000 46 | depends_on: 47 | - materialized 48 | frontend: 49 | container_name: frontend 50 | build: 51 | context: ./frontend 52 | ports: 53 | - 3000:3000 54 | depends_on: 55 | - backend 56 | microservice: 57 | container_name: microservice 58 | build: 59 | context: ./microservice 60 | depends_on: 61 | - backend 62 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Specify a base image 2 | FROM node:16 as build-deps 3 | 4 | # Create working directory and copy the app before running yarn install as the artifactory 5 | # credentials can be inside .npmrc 6 | WORKDIR /usr/src/app 7 | COPY . ./ 8 | 9 | # Run yarn install 10 | RUN yarn install 11 | 12 | # Build the project 13 | RUN yarn build 14 | 15 | # Install serve command for yarn package manager 16 | RUN yarn global add serve 17 | 18 | # Start the application 19 | CMD serve -p 3000 ./build -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with 2 | [Create React App](https://github.com/facebook/create-react-app). 3 | 4 | ## Available Scripts 5 | 6 | In the project directory, you can run: 7 | 8 | ### `yarn start` 9 | 10 | Runs the app in the development mode.
Open 11 | [http://localhost:3000](http://localhost:3000) to view it in the browser. 12 | 13 | The page will reload if you make edits.
You will also see any lint errors 14 | in the console. 15 | 16 | ### `yarn test` 17 | 18 | Launches the test runner in the interactive watch mode.
See the section 19 | about 20 | [running tests](https://facebook.github.io/create-react-app/docs/running-tests) 21 | for more information. 22 | 23 | ### `yarn build` 24 | 25 | Builds the app for production to the `build` folder.
It correctly bundles 26 | React in production mode and optimizes the build for the best performance. 27 | 28 | The build is minified and the filenames include the hashes.
Your app is 29 | ready to be deployed! 30 | 31 | See the section about 32 | [deployment](https://facebook.github.io/create-react-app/docs/deployment) for 33 | more information. 34 | 35 | ### `yarn eject` 36 | 37 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 38 | 39 | If you aren’t satisfied with the build tool and configuration choices, you can 40 | `eject` at any time. This command will remove the single build dependency from 41 | your project. 42 | 43 | Instead, it will copy all the configuration files and the transitive 44 | dependencies (webpack, Babel, ESLint, etc) right into your project so you have 45 | full control over them. All of the commands except `eject` will still work, but 46 | they will point to the copied scripts so you can tweak them. At this point 47 | you’re on your own. 48 | 49 | You don’t have to ever use `eject`. The curated feature set is suitable for 50 | small and middle deployments, and you shouldn’t feel obligated to use this 51 | feature. However we understand that this tool wouldn’t be useful if you couldn’t 52 | customize it when you are ready for it. 53 | 54 | ## Learn More 55 | 56 | You can learn more in the 57 | [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 58 | 59 | To learn React, check out the [React documentation](https://reactjs.org/). 60 | -------------------------------------------------------------------------------- /frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babel: { 3 | loaderOptions: { 4 | ignore: ["./node_modules/mapbox-gl/dist/mapbox-gl.js"], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.5.8", 7 | "@chakra-ui/react": "^1.7.4", 8 | "@craco/craco": "^6.4.3", 9 | "@emotion/react": "^11.0.0", 10 | "@emotion/styled": "^11.0.0", 11 | "@testing-library/jest-dom": "^5.9.0", 12 | "@testing-library/react": "^10.2.1", 13 | "@testing-library/user-event": "^12.0.2", 14 | "@types/jest": "^25.0.0", 15 | "@types/node": "^12.0.0", 16 | "@types/react": "^16.9.0", 17 | "@types/react-dom": "^16.9.0", 18 | "framer-motion": "^4.0.0", 19 | "graphql": "^16.2.0", 20 | "graphql-ws": "^5.5.5", 21 | "mapbox-gl": "^2.7.0", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2", 24 | "react-icons": "^3.0.0", 25 | "react-map-gl": "^6.1.19", 26 | "react-scripts": "5.0.0", 27 | "typescript": "^4.3.5", 28 | "web-vitals": "^0.2.2" 29 | }, 30 | "scripts": { 31 | "start": "craco start", 32 | "build": "craco build", 33 | "test": "craco test" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joacoc/antennas-manhattan/9626ff0e82caed24f93724f4b2ff70623ab22a90/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 22 | 26 | 27 | 36 | Antennas Performance 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joacoc/antennas-manhattan/9626ff0e82caed24f93724f4b2ff70623ab22a90/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joacoc/antennas-manhattan/9626ff0e82caed24f93724f4b2ff70623ab22a90/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { screen } from "@testing-library/react" 3 | import { render } from "./test-utils" 4 | import { App } from "./App" 5 | 6 | test("renders learn react link", () => { 7 | render() 8 | const linkElement = screen.getByText(/learn chakra/i) 9 | expect(linkElement).toBeInTheDocument() 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChakraProvider, Box, Text } from "@chakra-ui/react"; 3 | 4 | import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; 5 | import AntennasMap from "./components/AntennasMap"; 6 | import link from "./link"; 7 | import theme from "./theme"; 8 | 9 | const client = new ApolloClient({ 10 | uri: "backend:4000/graphql", 11 | cache: new InMemoryCache(), 12 | link, 13 | }); 14 | 15 | export const App = () => ( 16 | 17 | 18 | 25 | 26 | 🗽 Manhattan 5G Antennas Performance 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /frontend/src/ColorModeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | useColorMode, 4 | useColorModeValue, 5 | IconButton, 6 | IconButtonProps, 7 | } from "@chakra-ui/react" 8 | import { FaMoon, FaSun } from "react-icons/fa" 9 | 10 | type ColorModeSwitcherProps = Omit 11 | 12 | export const ColorModeSwitcher: React.FC = (props) => { 13 | const { toggleColorMode } = useColorMode() 14 | const text = useColorModeValue("dark", "light") 15 | const SwitchIcon = useColorModeValue(FaMoon, FaSun) 16 | 17 | return ( 18 | } 26 | aria-label={`Switch to ${text} mode`} 27 | {...props} 28 | /> 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | chakra, 4 | keyframes, 5 | ImageProps, 6 | forwardRef, 7 | usePrefersReducedMotion, 8 | } from "@chakra-ui/react" 9 | import logo from "./logo.svg" 10 | 11 | const spin = keyframes` 12 | from { transform: rotate(0deg); } 13 | to { transform: rotate(360deg); } 14 | ` 15 | 16 | export const Logo = forwardRef((props, ref) => { 17 | const prefersReducedMotion = usePrefersReducedMotion() 18 | 19 | const animation = prefersReducedMotion 20 | ? undefined 21 | : `${spin} infinite 20s linear` 22 | 23 | return 24 | }) 25 | -------------------------------------------------------------------------------- /frontend/src/components/AntennasMap/ButtonSelection/index.js: -------------------------------------------------------------------------------- 1 | export const buildButtonSelection = (mapBox) => { 2 | mapBox.on('idle', () => { 3 | // If these two layers were not added to the map, abort 4 | if (!mapBox.getLayer('healthy-antennas-layer') || !mapBox.getLayer('unhealthy-antennas-layer')) { 5 | return; 6 | } 7 | 8 | // Enumerate ids of the layers. 9 | const toggleableLayerIds = ['healthy-antennas-layer', 'unhealthy-antennas-layer']; 10 | 11 | // Set up the corresponding toggle button for each layer. 12 | toggleableLayerIds.forEach((layerId) => { 13 | // Skip layers that already have a button set up. 14 | if (document.getElementById(layerId)) { 15 | return; 16 | } 17 | 18 | // Create a link. 19 | const link = document.createElement('a'); 20 | link.id = layerId; 21 | link.href = '#'; 22 | link.textContent = layerId; 23 | link.className = 'active'; 24 | 25 | // Show or hide layer when the toggle is clicked. 26 | // eslint-disable-next-line no-loop-func 27 | link.onclick = function (e) { 28 | const clickedLayer = this.textContent; 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | 32 | const visibility = mapBox.getLayoutProperty( 33 | clickedLayer, 34 | 'visibility' 35 | ); 36 | 37 | // Toggle layer visibility by changing the layout object's visibility property. 38 | if (visibility === 'visible') { 39 | mapBox.setLayoutProperty(clickedLayer, 'visibility', 'none'); 40 | this.className = ''; 41 | } else { 42 | this.className = 'active'; 43 | mapBox.setLayoutProperty( 44 | clickedLayer, 45 | 'visibility', 46 | 'visible' 47 | ); 48 | } 49 | }; 50 | 51 | const layers = document.getElementById('menu'); 52 | layers.appendChild(link); 53 | }) 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /frontend/src/components/AntennasMap/DotClone/index.js: -------------------------------------------------------------------------------- 1 | export const buildPulsingDot = (map, color, size) => { 2 | return { 3 | width: size, 4 | height: size, 5 | data: new Uint8Array(size * size * 4), 6 | 7 | // get rendering context for the map canvas when layer is added to the map 8 | onAdd: function () { 9 | var canvas = document.createElement("canvas"); 10 | canvas.width = this.width; 11 | canvas.height = this.height; 12 | this.context = canvas.getContext("2d"); 13 | }, 14 | 15 | // called once before every frame where the icon will be used 16 | render: function () { 17 | var duration = 1750; 18 | var t = (performance.now() % duration) / duration; 19 | 20 | var radius = (size / 2) * 0.3; 21 | var outerRadius = (size / 2) * 0.7 * t + radius; 22 | var context = this.context; 23 | 24 | // draw outer circle 25 | context.clearRect(0, 0, this.width, this.height); 26 | context.beginPath(); 27 | context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2); 28 | context.fillStyle = `rgba(${color}, ${1 - t})`; 29 | context.fill(); 30 | 31 | // draw inner circle 32 | context.beginPath(); 33 | context.arc(this.width / 2, this.height / 2, radius / 3, 0, Math.PI * 2); 34 | context.fillStyle = `rgba(${color}, 1)`; 35 | context.fill(); 36 | 37 | // update this image's data with data from the canvas 38 | this.data = context.getImageData(0, 0, this.width, this.height).data; 39 | 40 | // continuously repaint the map, resulting in the smooth animation of the dot 41 | map.triggerRepaint(); 42 | 43 | // return `true` to let the map know that the image was updated 44 | return true; 45 | }, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/components/AntennasMap/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useRef, 3 | useEffect, 4 | useCallback, 5 | useMemo, 6 | useState, 7 | } from "react"; 8 | import { buildPulsingDot } from "./DotClone"; 9 | import { gql, useMutation, useSubscription } from "@apollo/client"; 10 | import { Button, Box, ListItem, Text, UnorderedList } from "@chakra-ui/react"; 11 | import mapboxgl, { Map as MapBox } from "mapbox-gl"; 12 | 13 | /** 14 | * Subscription 15 | */ 16 | const SUBSCRIBE_ANTENNAS = gql` 17 | subscription AntennasUpdates { 18 | antennasUpdates { 19 | antenna_id 20 | geojson 21 | performance 22 | diff 23 | timestamp 24 | } 25 | } 26 | `; 27 | 28 | /** 29 | * Mutation 30 | */ 31 | const MUTATE_ANTENNAS = gql` 32 | mutation Mutation($antenna_id: String!) { 33 | crashAntenna(antenna_id: $antenna_id) { 34 | antenna_id 35 | } 36 | } 37 | `; 38 | 39 | interface GeoJSON { 40 | type: string; 41 | geometry: { 42 | type: string; 43 | coordinates: [number, number]; 44 | }; 45 | properties: { 46 | name: string; 47 | helps?: string; 48 | }; 49 | } 50 | 51 | interface Antenna extends BaseAntenna { 52 | geojson: GeoJSON; 53 | } 54 | 55 | interface RawAntenna extends BaseAntenna { 56 | geojson: string; 57 | } 58 | 59 | interface BaseAntenna { 60 | antenna_id: string; 61 | performance: number; 62 | diff: number; 63 | timestamp: number; 64 | } 65 | 66 | interface AntennasUpdatesSubscription { 67 | antennasUpdates: Array; 68 | } 69 | 70 | /** 71 | * Set up a source data 72 | * @param map MapBox 73 | * @param sourceName Map source name 74 | * @param data Data to set 75 | */ 76 | function setSourceData( 77 | map: MapBox, 78 | sourceName: string, 79 | data: Array 80 | ): void { 81 | const source = map.getSource(sourceName); 82 | 83 | if (source) { 84 | (source as any).setData({ 85 | type: "FeatureCollection", 86 | features: data, 87 | }); 88 | } 89 | } 90 | 91 | /** 92 | * 93 | * @param map MapBox 94 | * @param name Image layer name 95 | * @param source Source name 96 | * @param color Color to be used 97 | */ 98 | function addPulsingDot( 99 | map: any, 100 | name: string, 101 | source: string, 102 | color: string, 103 | size?: number 104 | ) { 105 | (map.current as any).addImage( 106 | name, 107 | buildPulsingDot(map.current as any, color, size || 30), 108 | { 109 | pixelRatio: 2, 110 | } 111 | ); 112 | 113 | (map.current as any).addLayer({ 114 | id: name, 115 | type: "symbol", 116 | source: source, 117 | layout: { 118 | "icon-image": name, 119 | }, 120 | }); 121 | } 122 | 123 | /** 124 | * Replace with your own MapBox token 125 | */ 126 | function REPLACE_ME_WITH_YOUR_TOKEN() { 127 | return ( 128 | "pk" + 129 | ".ey" + 130 | "J1Ijo" + 131 | "iam9hcXVpbmNvbGFjY2kiLCJhIjoiY2t6N2Z4M2pzMWExcTJvdHYxc3k4MzFveSJ9.QSm7ZtegpUwuZ1MCbt4dIg" 132 | ); 133 | } 134 | 135 | /** 136 | * Tail updates require another step here to detect added and removed antennas 137 | * @param antennasMap 138 | * @param update 139 | */ 140 | function handleTailEventUpdate( 141 | event: Antenna, 142 | antennasMap: Map, 143 | markSet: Set, 144 | runSet: Set 145 | ) { 146 | const { 147 | antenna_id: antennaId, 148 | diff, 149 | timestamp: updateTimestamp, 150 | performance: antennaPerformance, 151 | } = event; 152 | markSet.delete(antennaId); 153 | const lastEvent = antennasMap.get(antennaId); 154 | 155 | if (lastEvent) { 156 | const { timestamp: lastTimestamp, performance: lastPerformance } = 157 | lastEvent; 158 | 159 | if (diff > 0 && lastTimestamp < updateTimestamp) { 160 | antennasMap.set(antennaId, event); 161 | } else if ( 162 | (lastPerformance === antennaPerformance && 163 | lastTimestamp <= updateTimestamp) || 164 | lastTimestamp < updateTimestamp 165 | ) { 166 | runSet.add(antennaId); 167 | } 168 | } else if (diff > 0) { 169 | antennasMap.set(antennaId, event); 170 | } 171 | } 172 | 173 | function createLayer( 174 | id: string, 175 | source: string, 176 | color: string 177 | ): mapboxgl.AnyLayer { 178 | return { 179 | id, 180 | type: "circle", 181 | source, 182 | paint: { 183 | "circle-radius": 70, 184 | "circle-color": color, 185 | "circle-opacity": 0.3, 186 | }, 187 | filter: ["==", "$type", "Point"], 188 | }; 189 | } 190 | 191 | /** 192 | * React component that renders antennas performance in a list and a map. 193 | * @returns 194 | */ 195 | export default function AntennasMap() { 196 | /** 197 | * References 198 | */ 199 | const mapContainer = useRef(null); 200 | const map = useRef(null); 201 | const dataRef = useRef(undefined); 202 | const [antennasMap, setAntennasMap] = useState>( 203 | new Map() 204 | ); 205 | const [antennasSupportedSet, setAntennasSupportedSet] = useState>( 206 | new Set() 207 | ); 208 | const markSetRef = useRef>(new Set()); 209 | 210 | /** 211 | * GraphQL Subscription 212 | */ 213 | const { error, data } = useSubscription( 214 | SUBSCRIBE_ANTENNAS, 215 | { fetchPolicy: "network-only" } 216 | ); 217 | 218 | /** 219 | * GraphQL Mutations 220 | */ 221 | const [mutateFunction, { error: mutationError }] = 222 | useMutation(MUTATE_ANTENNAS); 223 | 224 | /** 225 | * Layers Memo 226 | */ 227 | const typesOfAntennas = useMemo( 228 | () => [ 229 | { name: "healthy", layerColor: "#00FF00", dotColor: "0, 255, 0" }, 230 | { name: "unhealthy", layerColor: "#FF0000", dotColor: "255, 0, 0" }, 231 | { name: "semihealthy", layerColor: "#FFFF00", dotColor: "255, 255, 0" }, 232 | { name: "helper", layerColor: "#00FF00", dotColor: "0, 255, 0" }, 233 | ], 234 | [] 235 | ); 236 | 237 | const mainLayers = useMemo( 238 | () => [ 239 | "healthy-antennas-layer", 240 | "unhealthy-antennas-layer", 241 | "semihealthy-antennas-layer", 242 | "healthy-antennas-pulsing-dot", 243 | "unhealthy-antennas-pulsing-dot", 244 | "semihealthy-antennas-pulsing-dot", 245 | ], 246 | [] 247 | ); 248 | 249 | /** 250 | * GraphQL Errors logging 251 | */ 252 | if (error) { 253 | console.error(error); 254 | } 255 | 256 | if (mutationError) { 257 | console.error(mutationError); 258 | } 259 | 260 | /** 261 | * Callbacks & Handlers 262 | */ 263 | const onHighVoltageCrashClick = useCallback( 264 | (event) => { 265 | mutateFunction({ 266 | variables: { 267 | antenna_id: event.target.id, 268 | }, 269 | }); 270 | }, 271 | [mutateFunction] 272 | ); 273 | 274 | /** 275 | * Handle map antennas helpers filter 276 | */ 277 | const onHelpersClick = useCallback(() => { 278 | const { current: mapBox } = map; 279 | if (mapBox) { 280 | mapBox.setLayoutProperty( 281 | "helper-antennas-pulsing-dot", 282 | "visibility", 283 | "visible" 284 | ); 285 | 286 | mainLayers.forEach((layer) => 287 | mapBox.setLayoutProperty(layer, "visibility", "none") 288 | ); 289 | } 290 | }, [mainLayers]); 291 | 292 | /** 293 | * Handle map main antennas filter 294 | */ 295 | const onResetClick = useCallback(() => { 296 | const { current: mapBox } = map; 297 | if (mapBox) { 298 | mainLayers.forEach((layer) => { 299 | mapBox.setLayoutProperty(layer, "visibility", "visible"); 300 | mapBox.setFilter(layer); 301 | }); 302 | // Enable helpers too 303 | mapBox.setLayoutProperty( 304 | "helper-antennas-pulsing-dot", 305 | "visibility", 306 | "visible" 307 | ); 308 | } 309 | }, [mainLayers]); 310 | 311 | /** 312 | * Handle specific antenna filter 313 | */ 314 | const onAntennaClick = useCallback( 315 | (event) => { 316 | const { current: mapBox } = map; 317 | if (mapBox) { 318 | mainLayers.forEach((layer) => { 319 | const name = event.target.id; 320 | console.log(event.target); 321 | console.log("Filtering layer: ", layer, " - Name: ", name); 322 | mapBox.setFilter(layer, ["==", "name", name]); 323 | }); 324 | 325 | // Disable helpers too 326 | mapBox.setLayoutProperty( 327 | "helper-antennas-pulsing-dot", 328 | "visibility", 329 | "none" 330 | ); 331 | } 332 | }, 333 | [mainLayers] 334 | ); 335 | 336 | /** 337 | * Config Map 338 | */ 339 | const onLoad = useCallback(() => { 340 | /** 341 | * Set up antenna geojson's 342 | */ 343 | const { current: mapBox } = map; 344 | if (mapBox) { 345 | /** 346 | * Map sources 347 | */ 348 | typesOfAntennas.forEach(({ name, dotColor, layerColor }) => { 349 | /** 350 | * Add antenna source 351 | */ 352 | mapBox.addSource(`${name}-antennas`, { 353 | type: "geojson", 354 | data: { 355 | type: "FeatureCollection", 356 | features: [], 357 | }, 358 | }); 359 | 360 | /** 361 | * Add antenna layer 362 | */ 363 | if (name !== "helper") { 364 | mapBox.addLayer( 365 | createLayer( 366 | `${name}-antennas-layer`, 367 | `${name}-antennas`, 368 | layerColor 369 | ) 370 | ); 371 | } 372 | 373 | /** 374 | * Add antenna pulsating dot 375 | */ 376 | addPulsingDot( 377 | map, 378 | `${name}-antennas-pulsing-dot`, 379 | `${name}-antennas`, 380 | dotColor, 381 | 50 382 | ); 383 | }); 384 | } 385 | }, [typesOfAntennas]); 386 | 387 | /** 388 | * Use effects 389 | */ 390 | 391 | /** 392 | * Process data 393 | */ 394 | useEffect(() => { 395 | const { current: mapBox } = map; 396 | const { current: markSet } = markSetRef; 397 | 398 | /** 399 | * Only update when there is new data 400 | */ 401 | if (data && data !== dataRef.current) { 402 | dataRef.current = data; 403 | 404 | const { antennasUpdates: antennasUpdatesData } = data; 405 | 406 | if (antennasUpdatesData && antennasUpdatesData.length > 0) { 407 | /** 408 | * Set Up Antennas arrays 409 | */ 410 | const healthy: Array = []; 411 | const semiHealthy: Array = []; 412 | const unhealthy: Array = []; 413 | const helpers: Array = []; 414 | const runSet = new Set(); 415 | antennasSupportedSet.clear(); 416 | 417 | /** 418 | * Parse and update antennas performance 419 | */ 420 | antennasUpdatesData.forEach((antennaUpdate) => { 421 | try { 422 | const { geojson: rawGeoJson } = antennaUpdate; 423 | const geojson = JSON.parse(rawGeoJson); 424 | const antenna = { ...antennaUpdate, geojson }; 425 | geojson.type = "Feature"; 426 | 427 | handleTailEventUpdate(antenna, antennasMap, markSet, runSet); 428 | } catch (errParsing) { 429 | console.error(errParsing); 430 | } 431 | }); 432 | 433 | /** 434 | * Remove unused antennas 435 | */ 436 | markSet.forEach((markedEventId) => { 437 | console.log("Removing ", markedEventId); 438 | antennasMap.delete(markedEventId); 439 | }); 440 | markSetRef.current = runSet; 441 | 442 | /** 443 | * Flap helper antennas into one array 444 | */ 445 | Array.from(antennasMap.values()).forEach((antenna) => { 446 | const { antenna_id: antennaId, geojson, performance } = antenna; 447 | const { properties } = geojson; 448 | const { helps } = properties; 449 | 450 | if (!helps) { 451 | antennasMap.set(antennaId, antenna); 452 | if (performance > 5) { 453 | healthy.push(geojson); 454 | } else if (performance < 4.75) { 455 | unhealthy.push(geojson); 456 | } else { 457 | semiHealthy.push(geojson); 458 | } 459 | } else { 460 | helpers.push(geojson); 461 | antennasSupportedSet.add(helps); 462 | } 463 | }); 464 | 465 | if (mapBox) { 466 | setSourceData(mapBox, "healthy-antennas", healthy); 467 | setSourceData(mapBox, "unhealthy-antennas", unhealthy); 468 | setSourceData(mapBox, "semihealthy-antennas", semiHealthy); 469 | setSourceData(mapBox, "helper-antennas", helpers); 470 | } 471 | 472 | setAntennasMap(new Map(antennasMap)); 473 | setAntennasSupportedSet(new Set(antennasSupportedSet)); 474 | } 475 | } 476 | }, [antennasMap, antennasSupportedSet, data]); 477 | 478 | /** 479 | * Create the map 480 | */ 481 | useEffect(() => { 482 | const { current: mapBox } = map; 483 | if (mapBox) return; 484 | 485 | mapboxgl.accessToken = REPLACE_ME_WITH_YOUR_TOKEN(); 486 | (map.current as any) = new mapboxgl.Map({ 487 | container: "map", 488 | style: "mapbox://styles/mapbox/dark-v10", 489 | center: [-73.988, 40.733], 490 | zoom: 12.5, 491 | scrollZoom: false, 492 | doubleClickZoom: false, 493 | dragRotate: true, 494 | antialias: true, 495 | bearing: -60, 496 | }); 497 | 498 | (map.current as any).on("load", onLoad); 499 | }); 500 | 501 | return ( 502 | 503 | 510 | 517 | {Array.from(antennasMap.values()) 518 | .sort((a, b) => 519 | Number(a.antenna_id) > Number(b.antenna_id) ? 1 : -1 520 | ) 521 | .filter((x) => x.geojson.properties.helps === undefined) 522 | .map((x) => { 523 | return ( 524 | 525 | 526 | 534 | Performance:{" "} 535 | {x.performance.toString().substring(0, 4)} 536 | 537 | {antennasSupportedSet.has(x.geojson.properties.name) && ( 538 | 🛠️ 539 | )} 540 | 541 | 542 | 558 | 566 | 567 | 568 | ); 569 | })} 570 | 571 | 578 | 579 | 580 | 583 | 584 | 585 | 586 | {" "} 587 | Total antennas deployed: {antennasMap.size} 588 | 589 | 590 | 591 | ); 592 | } 593 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ColorModeScript } from "@chakra-ui/react" 2 | import * as React from "react" 3 | import ReactDOM from "react-dom" 4 | import { App } from "./App" 5 | import reportWebVitals from "./reportWebVitals" 6 | import * as serviceWorker from "./serviceWorker" 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | , 13 | document.getElementById("root"), 14 | ) 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://cra.link/PWA 19 | serviceWorker.unregister() 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals() 25 | -------------------------------------------------------------------------------- /frontend/src/link.ts: -------------------------------------------------------------------------------- 1 | // for Apollo Client v3: 2 | import { 3 | ApolloLink, 4 | Operation, 5 | FetchResult, 6 | Observable, 7 | } from "@apollo/client/core"; 8 | 9 | import { print } from "graphql"; 10 | import { createClient, ClientOptions, Client } from "graphql-ws"; 11 | 12 | class WebSocketLink extends ApolloLink { 13 | private client: Client; 14 | 15 | constructor(options: ClientOptions) { 16 | super(); 17 | this.client = createClient(options); 18 | } 19 | 20 | public request(operation: Operation): Observable { 21 | return new Observable((sink) => { 22 | return this.client.subscribe( 23 | { ...operation, query: print(operation.query) }, 24 | { 25 | next: sink.next.bind(sink) as any, 26 | complete: sink.complete.bind(sink), 27 | error: sink.error.bind(sink), 28 | } 29 | ); 30 | }); 31 | } 32 | } 33 | 34 | export default new WebSocketLink({ 35 | url: "ws://localhost:4000/graphql", 36 | }); 37 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals" 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ) 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void 26 | } 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) 32 | if (publicUrl.origin !== window.location.origin) { 33 | // Our service worker won't work if PUBLIC_URL is on a different origin 34 | // from what our page is served on. This might happen if a CDN is used to 35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 36 | return 37 | } 38 | 39 | window.addEventListener("load", () => { 40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 41 | 42 | if (isLocalhost) { 43 | // This is running on localhost. Let's check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl, config) 45 | 46 | // Add some additional logging to localhost, pointing developers to the 47 | // service worker/PWA documentation. 48 | navigator.serviceWorker.ready.then(() => { 49 | console.log( 50 | "This web app is being served cache-first by a service " + 51 | "worker. To learn more, visit https://cra.link/PWA", 52 | ) 53 | }) 54 | } else { 55 | // Is not localhost. Just register service worker 56 | registerValidSW(swUrl, config) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | function registerValidSW(swUrl: string, config?: Config) { 63 | navigator.serviceWorker 64 | .register(swUrl) 65 | .then((registration) => { 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing 68 | if (installingWorker == null) { 69 | return 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === "installed") { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | "New content is available and will be used when all " + 79 | "tabs for this page are closed. See https://cra.link/PWA.", 80 | ) 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration) 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It is the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log("Content is cached for offline use.") 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | }) 101 | .catch((error) => { 102 | console.error("Error during service worker registration:", error) 103 | }) 104 | } 105 | 106 | function checkValidServiceWorker(swUrl: string, config?: Config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl, { 109 | headers: { "Service-Worker": "script" }, 110 | }) 111 | .then((response) => { 112 | // Ensure service worker exists, and that we really are getting a JS file. 113 | const contentType = response.headers.get("content-type") 114 | if ( 115 | response.status === 404 || 116 | (contentType != null && contentType.indexOf("javascript") === -1) 117 | ) { 118 | // No service worker found. Probably a different app. Reload the page. 119 | navigator.serviceWorker.ready.then((registration) => { 120 | registration.unregister().then(() => { 121 | window.location.reload() 122 | }) 123 | }) 124 | } else { 125 | // Service worker found. Proceed as normal. 126 | registerValidSW(swUrl, config) 127 | } 128 | }) 129 | .catch(() => { 130 | console.log( 131 | "No internet connection found. App is running in offline mode.", 132 | ) 133 | }) 134 | } 135 | 136 | export function unregister() { 137 | if ("serviceWorker" in navigator) { 138 | navigator.serviceWorker.ready 139 | .then((registration) => { 140 | registration.unregister() 141 | }) 142 | .catch((error) => { 143 | console.error(error.message) 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom" 6 | -------------------------------------------------------------------------------- /frontend/src/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { render, RenderOptions } from "@testing-library/react" 3 | import { ChakraProvider, theme } from "@chakra-ui/react" 4 | 5 | const AllProviders = ({ children }: { children?: React.ReactNode }) => ( 6 | {children} 7 | ) 8 | 9 | const customRender = (ui: React.ReactElement, options?: RenderOptions) => 10 | render(ui, { wrapper: AllProviders, ...options }) 11 | 12 | export { customRender as render } 13 | -------------------------------------------------------------------------------- /frontend/src/theme.ts: -------------------------------------------------------------------------------- 1 | // 1. import `extendTheme` function 2 | import { extendTheme, ThemeConfig } from "@chakra-ui/react"; 3 | 4 | // 2. Add your color mode config 5 | const config: ThemeConfig = { 6 | initialColorMode: "dark", 7 | useSystemColorMode: false, 8 | }; 9 | 10 | // 3. extend the theme 11 | const theme = extendTheme({ config }); 12 | 13 | export default theme; 14 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | module.export = { 3 | // ... 4 | resolve: { 5 | alias: { 6 | "mapbox-gl": "maplibre-gl", 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /helper/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /helper/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /helper/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | 13 | # Bundle app source 14 | COPY . . 15 | 16 | EXPOSE 4000 17 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /helper/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "backend", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@types/pg": "^8.6.4", 13 | "pg": "^8.7.1" 14 | }, 15 | "devDependencies": { 16 | "@typescript-eslint/eslint-plugin": "^5.10.1", 17 | "@typescript-eslint/parser": "^5.10.1", 18 | "eslint": "^8.7.0", 19 | "typescript": "^4.5.5" 20 | } 21 | }, 22 | "node_modules/@eslint/eslintrc": { 23 | "version": "1.0.5", 24 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", 25 | "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", 26 | "dev": true, 27 | "dependencies": { 28 | "ajv": "^6.12.4", 29 | "debug": "^4.3.2", 30 | "espree": "^9.2.0", 31 | "globals": "^13.9.0", 32 | "ignore": "^4.0.6", 33 | "import-fresh": "^3.2.1", 34 | "js-yaml": "^4.1.0", 35 | "minimatch": "^3.0.4", 36 | "strip-json-comments": "^3.1.1" 37 | }, 38 | "engines": { 39 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 40 | } 41 | }, 42 | "node_modules/@eslint/eslintrc/node_modules/ignore": { 43 | "version": "4.0.6", 44 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 45 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 46 | "dev": true, 47 | "engines": { 48 | "node": ">= 4" 49 | } 50 | }, 51 | "node_modules/@humanwhocodes/config-array": { 52 | "version": "0.9.3", 53 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", 54 | "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", 55 | "dev": true, 56 | "dependencies": { 57 | "@humanwhocodes/object-schema": "^1.2.1", 58 | "debug": "^4.1.1", 59 | "minimatch": "^3.0.4" 60 | }, 61 | "engines": { 62 | "node": ">=10.10.0" 63 | } 64 | }, 65 | "node_modules/@humanwhocodes/object-schema": { 66 | "version": "1.2.1", 67 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 68 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 69 | "dev": true 70 | }, 71 | "node_modules/@nodelib/fs.scandir": { 72 | "version": "2.1.5", 73 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 74 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 75 | "dev": true, 76 | "dependencies": { 77 | "@nodelib/fs.stat": "2.0.5", 78 | "run-parallel": "^1.1.9" 79 | }, 80 | "engines": { 81 | "node": ">= 8" 82 | } 83 | }, 84 | "node_modules/@nodelib/fs.stat": { 85 | "version": "2.0.5", 86 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 87 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 88 | "dev": true, 89 | "engines": { 90 | "node": ">= 8" 91 | } 92 | }, 93 | "node_modules/@nodelib/fs.walk": { 94 | "version": "1.2.8", 95 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 96 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 97 | "dev": true, 98 | "dependencies": { 99 | "@nodelib/fs.scandir": "2.1.5", 100 | "fastq": "^1.6.0" 101 | }, 102 | "engines": { 103 | "node": ">= 8" 104 | } 105 | }, 106 | "node_modules/@types/json-schema": { 107 | "version": "7.0.9", 108 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", 109 | "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", 110 | "dev": true 111 | }, 112 | "node_modules/@types/node": { 113 | "version": "17.0.14", 114 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz", 115 | "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==" 116 | }, 117 | "node_modules/@types/pg": { 118 | "version": "8.6.4", 119 | "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.4.tgz", 120 | "integrity": "sha512-uYA7UMVzDFpJobCrqwW/iWkFmvizy6knIUgr0Quaw7K1Le3ZnF7hI3bKqFoxPZ+fju1Sc7zdTvOl9YfFZPcmeA==", 121 | "dependencies": { 122 | "@types/node": "*", 123 | "pg-protocol": "*", 124 | "pg-types": "^2.2.0" 125 | } 126 | }, 127 | "node_modules/@typescript-eslint/eslint-plugin": { 128 | "version": "5.10.2", 129 | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.2.tgz", 130 | "integrity": "sha512-4W/9lLuE+v27O/oe7hXJKjNtBLnZE8tQAFpapdxwSVHqtmIoPB1gph3+ahNwVuNL37BX7YQHyGF9Xv6XCnIX2Q==", 131 | "dev": true, 132 | "dependencies": { 133 | "@typescript-eslint/scope-manager": "5.10.2", 134 | "@typescript-eslint/type-utils": "5.10.2", 135 | "@typescript-eslint/utils": "5.10.2", 136 | "debug": "^4.3.2", 137 | "functional-red-black-tree": "^1.0.1", 138 | "ignore": "^5.1.8", 139 | "regexpp": "^3.2.0", 140 | "semver": "^7.3.5", 141 | "tsutils": "^3.21.0" 142 | }, 143 | "engines": { 144 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 145 | }, 146 | "funding": { 147 | "type": "opencollective", 148 | "url": "https://opencollective.com/typescript-eslint" 149 | }, 150 | "peerDependencies": { 151 | "@typescript-eslint/parser": "^5.0.0", 152 | "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" 153 | }, 154 | "peerDependenciesMeta": { 155 | "typescript": { 156 | "optional": true 157 | } 158 | } 159 | }, 160 | "node_modules/@typescript-eslint/parser": { 161 | "version": "5.10.2", 162 | "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.2.tgz", 163 | "integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==", 164 | "dev": true, 165 | "dependencies": { 166 | "@typescript-eslint/scope-manager": "5.10.2", 167 | "@typescript-eslint/types": "5.10.2", 168 | "@typescript-eslint/typescript-estree": "5.10.2", 169 | "debug": "^4.3.2" 170 | }, 171 | "engines": { 172 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 173 | }, 174 | "funding": { 175 | "type": "opencollective", 176 | "url": "https://opencollective.com/typescript-eslint" 177 | }, 178 | "peerDependencies": { 179 | "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" 180 | }, 181 | "peerDependenciesMeta": { 182 | "typescript": { 183 | "optional": true 184 | } 185 | } 186 | }, 187 | "node_modules/@typescript-eslint/scope-manager": { 188 | "version": "5.10.2", 189 | "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", 190 | "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", 191 | "dev": true, 192 | "dependencies": { 193 | "@typescript-eslint/types": "5.10.2", 194 | "@typescript-eslint/visitor-keys": "5.10.2" 195 | }, 196 | "engines": { 197 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 198 | }, 199 | "funding": { 200 | "type": "opencollective", 201 | "url": "https://opencollective.com/typescript-eslint" 202 | } 203 | }, 204 | "node_modules/@typescript-eslint/type-utils": { 205 | "version": "5.10.2", 206 | "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.2.tgz", 207 | "integrity": "sha512-uRKSvw/Ccs5FYEoXW04Z5VfzF2iiZcx8Fu7DGIB7RHozuP0VbKNzP1KfZkHBTM75pCpsWxIthEH1B33dmGBKHw==", 208 | "dev": true, 209 | "dependencies": { 210 | "@typescript-eslint/utils": "5.10.2", 211 | "debug": "^4.3.2", 212 | "tsutils": "^3.21.0" 213 | }, 214 | "engines": { 215 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 216 | }, 217 | "funding": { 218 | "type": "opencollective", 219 | "url": "https://opencollective.com/typescript-eslint" 220 | }, 221 | "peerDependencies": { 222 | "eslint": "*" 223 | }, 224 | "peerDependenciesMeta": { 225 | "typescript": { 226 | "optional": true 227 | } 228 | } 229 | }, 230 | "node_modules/@typescript-eslint/types": { 231 | "version": "5.10.2", 232 | "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", 233 | "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", 234 | "dev": true, 235 | "engines": { 236 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 237 | }, 238 | "funding": { 239 | "type": "opencollective", 240 | "url": "https://opencollective.com/typescript-eslint" 241 | } 242 | }, 243 | "node_modules/@typescript-eslint/typescript-estree": { 244 | "version": "5.10.2", 245 | "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", 246 | "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", 247 | "dev": true, 248 | "dependencies": { 249 | "@typescript-eslint/types": "5.10.2", 250 | "@typescript-eslint/visitor-keys": "5.10.2", 251 | "debug": "^4.3.2", 252 | "globby": "^11.0.4", 253 | "is-glob": "^4.0.3", 254 | "semver": "^7.3.5", 255 | "tsutils": "^3.21.0" 256 | }, 257 | "engines": { 258 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 259 | }, 260 | "funding": { 261 | "type": "opencollective", 262 | "url": "https://opencollective.com/typescript-eslint" 263 | }, 264 | "peerDependenciesMeta": { 265 | "typescript": { 266 | "optional": true 267 | } 268 | } 269 | }, 270 | "node_modules/@typescript-eslint/utils": { 271 | "version": "5.10.2", 272 | "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", 273 | "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", 274 | "dev": true, 275 | "dependencies": { 276 | "@types/json-schema": "^7.0.9", 277 | "@typescript-eslint/scope-manager": "5.10.2", 278 | "@typescript-eslint/types": "5.10.2", 279 | "@typescript-eslint/typescript-estree": "5.10.2", 280 | "eslint-scope": "^5.1.1", 281 | "eslint-utils": "^3.0.0" 282 | }, 283 | "engines": { 284 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 285 | }, 286 | "funding": { 287 | "type": "opencollective", 288 | "url": "https://opencollective.com/typescript-eslint" 289 | }, 290 | "peerDependencies": { 291 | "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" 292 | } 293 | }, 294 | "node_modules/@typescript-eslint/visitor-keys": { 295 | "version": "5.10.2", 296 | "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", 297 | "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", 298 | "dev": true, 299 | "dependencies": { 300 | "@typescript-eslint/types": "5.10.2", 301 | "eslint-visitor-keys": "^3.0.0" 302 | }, 303 | "engines": { 304 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 305 | }, 306 | "funding": { 307 | "type": "opencollective", 308 | "url": "https://opencollective.com/typescript-eslint" 309 | } 310 | }, 311 | "node_modules/acorn": { 312 | "version": "8.7.0", 313 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", 314 | "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", 315 | "dev": true, 316 | "bin": { 317 | "acorn": "bin/acorn" 318 | }, 319 | "engines": { 320 | "node": ">=0.4.0" 321 | } 322 | }, 323 | "node_modules/acorn-jsx": { 324 | "version": "5.3.2", 325 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 326 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 327 | "dev": true, 328 | "peerDependencies": { 329 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 330 | } 331 | }, 332 | "node_modules/ajv": { 333 | "version": "6.12.6", 334 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 335 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 336 | "dev": true, 337 | "dependencies": { 338 | "fast-deep-equal": "^3.1.1", 339 | "fast-json-stable-stringify": "^2.0.0", 340 | "json-schema-traverse": "^0.4.1", 341 | "uri-js": "^4.2.2" 342 | }, 343 | "funding": { 344 | "type": "github", 345 | "url": "https://github.com/sponsors/epoberezkin" 346 | } 347 | }, 348 | "node_modules/ansi-regex": { 349 | "version": "5.0.1", 350 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 351 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 352 | "dev": true, 353 | "engines": { 354 | "node": ">=8" 355 | } 356 | }, 357 | "node_modules/ansi-styles": { 358 | "version": "4.3.0", 359 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 360 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 361 | "dev": true, 362 | "dependencies": { 363 | "color-convert": "^2.0.1" 364 | }, 365 | "engines": { 366 | "node": ">=8" 367 | }, 368 | "funding": { 369 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 370 | } 371 | }, 372 | "node_modules/argparse": { 373 | "version": "2.0.1", 374 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 375 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 376 | "dev": true 377 | }, 378 | "node_modules/array-union": { 379 | "version": "2.1.0", 380 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", 381 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", 382 | "dev": true, 383 | "engines": { 384 | "node": ">=8" 385 | } 386 | }, 387 | "node_modules/balanced-match": { 388 | "version": "1.0.2", 389 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 390 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 391 | "dev": true 392 | }, 393 | "node_modules/brace-expansion": { 394 | "version": "1.1.11", 395 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 396 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 397 | "dev": true, 398 | "dependencies": { 399 | "balanced-match": "^1.0.0", 400 | "concat-map": "0.0.1" 401 | } 402 | }, 403 | "node_modules/braces": { 404 | "version": "3.0.2", 405 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 406 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 407 | "dev": true, 408 | "dependencies": { 409 | "fill-range": "^7.0.1" 410 | }, 411 | "engines": { 412 | "node": ">=8" 413 | } 414 | }, 415 | "node_modules/buffer-writer": { 416 | "version": "2.0.0", 417 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 418 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", 419 | "engines": { 420 | "node": ">=4" 421 | } 422 | }, 423 | "node_modules/callsites": { 424 | "version": "3.1.0", 425 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 426 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 427 | "dev": true, 428 | "engines": { 429 | "node": ">=6" 430 | } 431 | }, 432 | "node_modules/chalk": { 433 | "version": "4.1.2", 434 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 435 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 436 | "dev": true, 437 | "dependencies": { 438 | "ansi-styles": "^4.1.0", 439 | "supports-color": "^7.1.0" 440 | }, 441 | "engines": { 442 | "node": ">=10" 443 | }, 444 | "funding": { 445 | "url": "https://github.com/chalk/chalk?sponsor=1" 446 | } 447 | }, 448 | "node_modules/color-convert": { 449 | "version": "2.0.1", 450 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 451 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 452 | "dev": true, 453 | "dependencies": { 454 | "color-name": "~1.1.4" 455 | }, 456 | "engines": { 457 | "node": ">=7.0.0" 458 | } 459 | }, 460 | "node_modules/color-name": { 461 | "version": "1.1.4", 462 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 463 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 464 | "dev": true 465 | }, 466 | "node_modules/concat-map": { 467 | "version": "0.0.1", 468 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 469 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 470 | "dev": true 471 | }, 472 | "node_modules/cross-spawn": { 473 | "version": "7.0.3", 474 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 475 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 476 | "dev": true, 477 | "dependencies": { 478 | "path-key": "^3.1.0", 479 | "shebang-command": "^2.0.0", 480 | "which": "^2.0.1" 481 | }, 482 | "engines": { 483 | "node": ">= 8" 484 | } 485 | }, 486 | "node_modules/debug": { 487 | "version": "4.3.3", 488 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 489 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 490 | "dev": true, 491 | "dependencies": { 492 | "ms": "2.1.2" 493 | }, 494 | "engines": { 495 | "node": ">=6.0" 496 | }, 497 | "peerDependenciesMeta": { 498 | "supports-color": { 499 | "optional": true 500 | } 501 | } 502 | }, 503 | "node_modules/deep-is": { 504 | "version": "0.1.4", 505 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 506 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 507 | "dev": true 508 | }, 509 | "node_modules/dir-glob": { 510 | "version": "3.0.1", 511 | "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", 512 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", 513 | "dev": true, 514 | "dependencies": { 515 | "path-type": "^4.0.0" 516 | }, 517 | "engines": { 518 | "node": ">=8" 519 | } 520 | }, 521 | "node_modules/doctrine": { 522 | "version": "3.0.0", 523 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 524 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 525 | "dev": true, 526 | "dependencies": { 527 | "esutils": "^2.0.2" 528 | }, 529 | "engines": { 530 | "node": ">=6.0.0" 531 | } 532 | }, 533 | "node_modules/escape-string-regexp": { 534 | "version": "4.0.0", 535 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 536 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 537 | "dev": true, 538 | "engines": { 539 | "node": ">=10" 540 | }, 541 | "funding": { 542 | "url": "https://github.com/sponsors/sindresorhus" 543 | } 544 | }, 545 | "node_modules/eslint": { 546 | "version": "8.8.0", 547 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", 548 | "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", 549 | "dev": true, 550 | "dependencies": { 551 | "@eslint/eslintrc": "^1.0.5", 552 | "@humanwhocodes/config-array": "^0.9.2", 553 | "ajv": "^6.10.0", 554 | "chalk": "^4.0.0", 555 | "cross-spawn": "^7.0.2", 556 | "debug": "^4.3.2", 557 | "doctrine": "^3.0.0", 558 | "escape-string-regexp": "^4.0.0", 559 | "eslint-scope": "^7.1.0", 560 | "eslint-utils": "^3.0.0", 561 | "eslint-visitor-keys": "^3.2.0", 562 | "espree": "^9.3.0", 563 | "esquery": "^1.4.0", 564 | "esutils": "^2.0.2", 565 | "fast-deep-equal": "^3.1.3", 566 | "file-entry-cache": "^6.0.1", 567 | "functional-red-black-tree": "^1.0.1", 568 | "glob-parent": "^6.0.1", 569 | "globals": "^13.6.0", 570 | "ignore": "^5.2.0", 571 | "import-fresh": "^3.0.0", 572 | "imurmurhash": "^0.1.4", 573 | "is-glob": "^4.0.0", 574 | "js-yaml": "^4.1.0", 575 | "json-stable-stringify-without-jsonify": "^1.0.1", 576 | "levn": "^0.4.1", 577 | "lodash.merge": "^4.6.2", 578 | "minimatch": "^3.0.4", 579 | "natural-compare": "^1.4.0", 580 | "optionator": "^0.9.1", 581 | "regexpp": "^3.2.0", 582 | "strip-ansi": "^6.0.1", 583 | "strip-json-comments": "^3.1.0", 584 | "text-table": "^0.2.0", 585 | "v8-compile-cache": "^2.0.3" 586 | }, 587 | "bin": { 588 | "eslint": "bin/eslint.js" 589 | }, 590 | "engines": { 591 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 592 | }, 593 | "funding": { 594 | "url": "https://opencollective.com/eslint" 595 | } 596 | }, 597 | "node_modules/eslint-scope": { 598 | "version": "5.1.1", 599 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", 600 | "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", 601 | "dev": true, 602 | "dependencies": { 603 | "esrecurse": "^4.3.0", 604 | "estraverse": "^4.1.1" 605 | }, 606 | "engines": { 607 | "node": ">=8.0.0" 608 | } 609 | }, 610 | "node_modules/eslint-utils": { 611 | "version": "3.0.0", 612 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", 613 | "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", 614 | "dev": true, 615 | "dependencies": { 616 | "eslint-visitor-keys": "^2.0.0" 617 | }, 618 | "engines": { 619 | "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" 620 | }, 621 | "funding": { 622 | "url": "https://github.com/sponsors/mysticatea" 623 | }, 624 | "peerDependencies": { 625 | "eslint": ">=5" 626 | } 627 | }, 628 | "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { 629 | "version": "2.1.0", 630 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", 631 | "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", 632 | "dev": true, 633 | "engines": { 634 | "node": ">=10" 635 | } 636 | }, 637 | "node_modules/eslint-visitor-keys": { 638 | "version": "3.2.0", 639 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", 640 | "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", 641 | "dev": true, 642 | "engines": { 643 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 644 | } 645 | }, 646 | "node_modules/eslint/node_modules/eslint-scope": { 647 | "version": "7.1.0", 648 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", 649 | "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", 650 | "dev": true, 651 | "dependencies": { 652 | "esrecurse": "^4.3.0", 653 | "estraverse": "^5.2.0" 654 | }, 655 | "engines": { 656 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 657 | } 658 | }, 659 | "node_modules/eslint/node_modules/estraverse": { 660 | "version": "5.3.0", 661 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 662 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 663 | "dev": true, 664 | "engines": { 665 | "node": ">=4.0" 666 | } 667 | }, 668 | "node_modules/espree": { 669 | "version": "9.3.0", 670 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", 671 | "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", 672 | "dev": true, 673 | "dependencies": { 674 | "acorn": "^8.7.0", 675 | "acorn-jsx": "^5.3.1", 676 | "eslint-visitor-keys": "^3.1.0" 677 | }, 678 | "engines": { 679 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 680 | } 681 | }, 682 | "node_modules/esquery": { 683 | "version": "1.4.0", 684 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", 685 | "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", 686 | "dev": true, 687 | "dependencies": { 688 | "estraverse": "^5.1.0" 689 | }, 690 | "engines": { 691 | "node": ">=0.10" 692 | } 693 | }, 694 | "node_modules/esquery/node_modules/estraverse": { 695 | "version": "5.3.0", 696 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 697 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 698 | "dev": true, 699 | "engines": { 700 | "node": ">=4.0" 701 | } 702 | }, 703 | "node_modules/esrecurse": { 704 | "version": "4.3.0", 705 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 706 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 707 | "dev": true, 708 | "dependencies": { 709 | "estraverse": "^5.2.0" 710 | }, 711 | "engines": { 712 | "node": ">=4.0" 713 | } 714 | }, 715 | "node_modules/esrecurse/node_modules/estraverse": { 716 | "version": "5.3.0", 717 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 718 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 719 | "dev": true, 720 | "engines": { 721 | "node": ">=4.0" 722 | } 723 | }, 724 | "node_modules/estraverse": { 725 | "version": "4.3.0", 726 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 727 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 728 | "dev": true, 729 | "engines": { 730 | "node": ">=4.0" 731 | } 732 | }, 733 | "node_modules/esutils": { 734 | "version": "2.0.3", 735 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 736 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 737 | "dev": true, 738 | "engines": { 739 | "node": ">=0.10.0" 740 | } 741 | }, 742 | "node_modules/fast-deep-equal": { 743 | "version": "3.1.3", 744 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 745 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 746 | "dev": true 747 | }, 748 | "node_modules/fast-glob": { 749 | "version": "3.2.11", 750 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", 751 | "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", 752 | "dev": true, 753 | "dependencies": { 754 | "@nodelib/fs.stat": "^2.0.2", 755 | "@nodelib/fs.walk": "^1.2.3", 756 | "glob-parent": "^5.1.2", 757 | "merge2": "^1.3.0", 758 | "micromatch": "^4.0.4" 759 | }, 760 | "engines": { 761 | "node": ">=8.6.0" 762 | } 763 | }, 764 | "node_modules/fast-glob/node_modules/glob-parent": { 765 | "version": "5.1.2", 766 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 767 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 768 | "dev": true, 769 | "dependencies": { 770 | "is-glob": "^4.0.1" 771 | }, 772 | "engines": { 773 | "node": ">= 6" 774 | } 775 | }, 776 | "node_modules/fast-json-stable-stringify": { 777 | "version": "2.1.0", 778 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 779 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 780 | "dev": true 781 | }, 782 | "node_modules/fast-levenshtein": { 783 | "version": "2.0.6", 784 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 785 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 786 | "dev": true 787 | }, 788 | "node_modules/fastq": { 789 | "version": "1.13.0", 790 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 791 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 792 | "dev": true, 793 | "dependencies": { 794 | "reusify": "^1.0.4" 795 | } 796 | }, 797 | "node_modules/file-entry-cache": { 798 | "version": "6.0.1", 799 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 800 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 801 | "dev": true, 802 | "dependencies": { 803 | "flat-cache": "^3.0.4" 804 | }, 805 | "engines": { 806 | "node": "^10.12.0 || >=12.0.0" 807 | } 808 | }, 809 | "node_modules/fill-range": { 810 | "version": "7.0.1", 811 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 812 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 813 | "dev": true, 814 | "dependencies": { 815 | "to-regex-range": "^5.0.1" 816 | }, 817 | "engines": { 818 | "node": ">=8" 819 | } 820 | }, 821 | "node_modules/flat-cache": { 822 | "version": "3.0.4", 823 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 824 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 825 | "dev": true, 826 | "dependencies": { 827 | "flatted": "^3.1.0", 828 | "rimraf": "^3.0.2" 829 | }, 830 | "engines": { 831 | "node": "^10.12.0 || >=12.0.0" 832 | } 833 | }, 834 | "node_modules/flatted": { 835 | "version": "3.2.5", 836 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", 837 | "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", 838 | "dev": true 839 | }, 840 | "node_modules/fs.realpath": { 841 | "version": "1.0.0", 842 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 843 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 844 | "dev": true 845 | }, 846 | "node_modules/functional-red-black-tree": { 847 | "version": "1.0.1", 848 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 849 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 850 | "dev": true 851 | }, 852 | "node_modules/glob": { 853 | "version": "7.2.0", 854 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 855 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 856 | "dev": true, 857 | "dependencies": { 858 | "fs.realpath": "^1.0.0", 859 | "inflight": "^1.0.4", 860 | "inherits": "2", 861 | "minimatch": "^3.0.4", 862 | "once": "^1.3.0", 863 | "path-is-absolute": "^1.0.0" 864 | }, 865 | "engines": { 866 | "node": "*" 867 | }, 868 | "funding": { 869 | "url": "https://github.com/sponsors/isaacs" 870 | } 871 | }, 872 | "node_modules/glob-parent": { 873 | "version": "6.0.2", 874 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 875 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 876 | "dev": true, 877 | "dependencies": { 878 | "is-glob": "^4.0.3" 879 | }, 880 | "engines": { 881 | "node": ">=10.13.0" 882 | } 883 | }, 884 | "node_modules/globals": { 885 | "version": "13.12.1", 886 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", 887 | "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", 888 | "dev": true, 889 | "dependencies": { 890 | "type-fest": "^0.20.2" 891 | }, 892 | "engines": { 893 | "node": ">=8" 894 | }, 895 | "funding": { 896 | "url": "https://github.com/sponsors/sindresorhus" 897 | } 898 | }, 899 | "node_modules/globby": { 900 | "version": "11.1.0", 901 | "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", 902 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", 903 | "dev": true, 904 | "dependencies": { 905 | "array-union": "^2.1.0", 906 | "dir-glob": "^3.0.1", 907 | "fast-glob": "^3.2.9", 908 | "ignore": "^5.2.0", 909 | "merge2": "^1.4.1", 910 | "slash": "^3.0.0" 911 | }, 912 | "engines": { 913 | "node": ">=10" 914 | }, 915 | "funding": { 916 | "url": "https://github.com/sponsors/sindresorhus" 917 | } 918 | }, 919 | "node_modules/has-flag": { 920 | "version": "4.0.0", 921 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 922 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 923 | "dev": true, 924 | "engines": { 925 | "node": ">=8" 926 | } 927 | }, 928 | "node_modules/ignore": { 929 | "version": "5.2.0", 930 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 931 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", 932 | "dev": true, 933 | "engines": { 934 | "node": ">= 4" 935 | } 936 | }, 937 | "node_modules/import-fresh": { 938 | "version": "3.3.0", 939 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 940 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 941 | "dev": true, 942 | "dependencies": { 943 | "parent-module": "^1.0.0", 944 | "resolve-from": "^4.0.0" 945 | }, 946 | "engines": { 947 | "node": ">=6" 948 | }, 949 | "funding": { 950 | "url": "https://github.com/sponsors/sindresorhus" 951 | } 952 | }, 953 | "node_modules/imurmurhash": { 954 | "version": "0.1.4", 955 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 956 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 957 | "dev": true, 958 | "engines": { 959 | "node": ">=0.8.19" 960 | } 961 | }, 962 | "node_modules/inflight": { 963 | "version": "1.0.6", 964 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 965 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 966 | "dev": true, 967 | "dependencies": { 968 | "once": "^1.3.0", 969 | "wrappy": "1" 970 | } 971 | }, 972 | "node_modules/inherits": { 973 | "version": "2.0.4", 974 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 975 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 976 | "dev": true 977 | }, 978 | "node_modules/is-extglob": { 979 | "version": "2.1.1", 980 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 981 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 982 | "dev": true, 983 | "engines": { 984 | "node": ">=0.10.0" 985 | } 986 | }, 987 | "node_modules/is-glob": { 988 | "version": "4.0.3", 989 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 990 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 991 | "dev": true, 992 | "dependencies": { 993 | "is-extglob": "^2.1.1" 994 | }, 995 | "engines": { 996 | "node": ">=0.10.0" 997 | } 998 | }, 999 | "node_modules/is-number": { 1000 | "version": "7.0.0", 1001 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1002 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1003 | "dev": true, 1004 | "engines": { 1005 | "node": ">=0.12.0" 1006 | } 1007 | }, 1008 | "node_modules/isexe": { 1009 | "version": "2.0.0", 1010 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1011 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1012 | "dev": true 1013 | }, 1014 | "node_modules/js-yaml": { 1015 | "version": "4.1.0", 1016 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1017 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1018 | "dev": true, 1019 | "dependencies": { 1020 | "argparse": "^2.0.1" 1021 | }, 1022 | "bin": { 1023 | "js-yaml": "bin/js-yaml.js" 1024 | } 1025 | }, 1026 | "node_modules/json-schema-traverse": { 1027 | "version": "0.4.1", 1028 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1029 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1030 | "dev": true 1031 | }, 1032 | "node_modules/json-stable-stringify-without-jsonify": { 1033 | "version": "1.0.1", 1034 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1035 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 1036 | "dev": true 1037 | }, 1038 | "node_modules/levn": { 1039 | "version": "0.4.1", 1040 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1041 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1042 | "dev": true, 1043 | "dependencies": { 1044 | "prelude-ls": "^1.2.1", 1045 | "type-check": "~0.4.0" 1046 | }, 1047 | "engines": { 1048 | "node": ">= 0.8.0" 1049 | } 1050 | }, 1051 | "node_modules/lodash.merge": { 1052 | "version": "4.6.2", 1053 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1054 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1055 | "dev": true 1056 | }, 1057 | "node_modules/lru-cache": { 1058 | "version": "6.0.0", 1059 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 1060 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 1061 | "dev": true, 1062 | "dependencies": { 1063 | "yallist": "^4.0.0" 1064 | }, 1065 | "engines": { 1066 | "node": ">=10" 1067 | } 1068 | }, 1069 | "node_modules/merge2": { 1070 | "version": "1.4.1", 1071 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1072 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1073 | "dev": true, 1074 | "engines": { 1075 | "node": ">= 8" 1076 | } 1077 | }, 1078 | "node_modules/micromatch": { 1079 | "version": "4.0.4", 1080 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 1081 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 1082 | "dev": true, 1083 | "dependencies": { 1084 | "braces": "^3.0.1", 1085 | "picomatch": "^2.2.3" 1086 | }, 1087 | "engines": { 1088 | "node": ">=8.6" 1089 | } 1090 | }, 1091 | "node_modules/minimatch": { 1092 | "version": "3.0.4", 1093 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1094 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1095 | "dev": true, 1096 | "dependencies": { 1097 | "brace-expansion": "^1.1.7" 1098 | }, 1099 | "engines": { 1100 | "node": "*" 1101 | } 1102 | }, 1103 | "node_modules/ms": { 1104 | "version": "2.1.2", 1105 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1106 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1107 | "dev": true 1108 | }, 1109 | "node_modules/natural-compare": { 1110 | "version": "1.4.0", 1111 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1112 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 1113 | "dev": true 1114 | }, 1115 | "node_modules/once": { 1116 | "version": "1.4.0", 1117 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1118 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1119 | "dev": true, 1120 | "dependencies": { 1121 | "wrappy": "1" 1122 | } 1123 | }, 1124 | "node_modules/optionator": { 1125 | "version": "0.9.1", 1126 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 1127 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 1128 | "dev": true, 1129 | "dependencies": { 1130 | "deep-is": "^0.1.3", 1131 | "fast-levenshtein": "^2.0.6", 1132 | "levn": "^0.4.1", 1133 | "prelude-ls": "^1.2.1", 1134 | "type-check": "^0.4.0", 1135 | "word-wrap": "^1.2.3" 1136 | }, 1137 | "engines": { 1138 | "node": ">= 0.8.0" 1139 | } 1140 | }, 1141 | "node_modules/packet-reader": { 1142 | "version": "1.0.0", 1143 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 1144 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 1145 | }, 1146 | "node_modules/parent-module": { 1147 | "version": "1.0.1", 1148 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1149 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1150 | "dev": true, 1151 | "dependencies": { 1152 | "callsites": "^3.0.0" 1153 | }, 1154 | "engines": { 1155 | "node": ">=6" 1156 | } 1157 | }, 1158 | "node_modules/path-is-absolute": { 1159 | "version": "1.0.1", 1160 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1161 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1162 | "dev": true, 1163 | "engines": { 1164 | "node": ">=0.10.0" 1165 | } 1166 | }, 1167 | "node_modules/path-key": { 1168 | "version": "3.1.1", 1169 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1170 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1171 | "dev": true, 1172 | "engines": { 1173 | "node": ">=8" 1174 | } 1175 | }, 1176 | "node_modules/path-type": { 1177 | "version": "4.0.0", 1178 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 1179 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 1180 | "dev": true, 1181 | "engines": { 1182 | "node": ">=8" 1183 | } 1184 | }, 1185 | "node_modules/pg": { 1186 | "version": "8.7.1", 1187 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", 1188 | "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", 1189 | "dependencies": { 1190 | "buffer-writer": "2.0.0", 1191 | "packet-reader": "1.0.0", 1192 | "pg-connection-string": "^2.5.0", 1193 | "pg-pool": "^3.4.1", 1194 | "pg-protocol": "^1.5.0", 1195 | "pg-types": "^2.1.0", 1196 | "pgpass": "1.x" 1197 | }, 1198 | "engines": { 1199 | "node": ">= 8.0.0" 1200 | }, 1201 | "peerDependencies": { 1202 | "pg-native": ">=2.0.0" 1203 | }, 1204 | "peerDependenciesMeta": { 1205 | "pg-native": { 1206 | "optional": true 1207 | } 1208 | } 1209 | }, 1210 | "node_modules/pg-connection-string": { 1211 | "version": "2.5.0", 1212 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", 1213 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" 1214 | }, 1215 | "node_modules/pg-int8": { 1216 | "version": "1.0.1", 1217 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 1218 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", 1219 | "engines": { 1220 | "node": ">=4.0.0" 1221 | } 1222 | }, 1223 | "node_modules/pg-pool": { 1224 | "version": "3.4.1", 1225 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", 1226 | "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", 1227 | "peerDependencies": { 1228 | "pg": ">=8.0" 1229 | } 1230 | }, 1231 | "node_modules/pg-protocol": { 1232 | "version": "1.5.0", 1233 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", 1234 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" 1235 | }, 1236 | "node_modules/pg-types": { 1237 | "version": "2.2.0", 1238 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 1239 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 1240 | "dependencies": { 1241 | "pg-int8": "1.0.1", 1242 | "postgres-array": "~2.0.0", 1243 | "postgres-bytea": "~1.0.0", 1244 | "postgres-date": "~1.0.4", 1245 | "postgres-interval": "^1.1.0" 1246 | }, 1247 | "engines": { 1248 | "node": ">=4" 1249 | } 1250 | }, 1251 | "node_modules/pgpass": { 1252 | "version": "1.0.5", 1253 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", 1254 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", 1255 | "dependencies": { 1256 | "split2": "^4.1.0" 1257 | } 1258 | }, 1259 | "node_modules/picomatch": { 1260 | "version": "2.3.1", 1261 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1262 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1263 | "dev": true, 1264 | "engines": { 1265 | "node": ">=8.6" 1266 | }, 1267 | "funding": { 1268 | "url": "https://github.com/sponsors/jonschlinkert" 1269 | } 1270 | }, 1271 | "node_modules/postgres-array": { 1272 | "version": "2.0.0", 1273 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 1274 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", 1275 | "engines": { 1276 | "node": ">=4" 1277 | } 1278 | }, 1279 | "node_modules/postgres-bytea": { 1280 | "version": "1.0.0", 1281 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 1282 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", 1283 | "engines": { 1284 | "node": ">=0.10.0" 1285 | } 1286 | }, 1287 | "node_modules/postgres-date": { 1288 | "version": "1.0.7", 1289 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 1290 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", 1291 | "engines": { 1292 | "node": ">=0.10.0" 1293 | } 1294 | }, 1295 | "node_modules/postgres-interval": { 1296 | "version": "1.2.0", 1297 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 1298 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 1299 | "dependencies": { 1300 | "xtend": "^4.0.0" 1301 | }, 1302 | "engines": { 1303 | "node": ">=0.10.0" 1304 | } 1305 | }, 1306 | "node_modules/prelude-ls": { 1307 | "version": "1.2.1", 1308 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1309 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1310 | "dev": true, 1311 | "engines": { 1312 | "node": ">= 0.8.0" 1313 | } 1314 | }, 1315 | "node_modules/punycode": { 1316 | "version": "2.1.1", 1317 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1318 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1319 | "dev": true, 1320 | "engines": { 1321 | "node": ">=6" 1322 | } 1323 | }, 1324 | "node_modules/queue-microtask": { 1325 | "version": "1.2.3", 1326 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1327 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1328 | "dev": true, 1329 | "funding": [ 1330 | { 1331 | "type": "github", 1332 | "url": "https://github.com/sponsors/feross" 1333 | }, 1334 | { 1335 | "type": "patreon", 1336 | "url": "https://www.patreon.com/feross" 1337 | }, 1338 | { 1339 | "type": "consulting", 1340 | "url": "https://feross.org/support" 1341 | } 1342 | ] 1343 | }, 1344 | "node_modules/regexpp": { 1345 | "version": "3.2.0", 1346 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", 1347 | "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", 1348 | "dev": true, 1349 | "engines": { 1350 | "node": ">=8" 1351 | }, 1352 | "funding": { 1353 | "url": "https://github.com/sponsors/mysticatea" 1354 | } 1355 | }, 1356 | "node_modules/resolve-from": { 1357 | "version": "4.0.0", 1358 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1359 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1360 | "dev": true, 1361 | "engines": { 1362 | "node": ">=4" 1363 | } 1364 | }, 1365 | "node_modules/reusify": { 1366 | "version": "1.0.4", 1367 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1368 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1369 | "dev": true, 1370 | "engines": { 1371 | "iojs": ">=1.0.0", 1372 | "node": ">=0.10.0" 1373 | } 1374 | }, 1375 | "node_modules/rimraf": { 1376 | "version": "3.0.2", 1377 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1378 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1379 | "dev": true, 1380 | "dependencies": { 1381 | "glob": "^7.1.3" 1382 | }, 1383 | "bin": { 1384 | "rimraf": "bin.js" 1385 | }, 1386 | "funding": { 1387 | "url": "https://github.com/sponsors/isaacs" 1388 | } 1389 | }, 1390 | "node_modules/run-parallel": { 1391 | "version": "1.2.0", 1392 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1393 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1394 | "dev": true, 1395 | "funding": [ 1396 | { 1397 | "type": "github", 1398 | "url": "https://github.com/sponsors/feross" 1399 | }, 1400 | { 1401 | "type": "patreon", 1402 | "url": "https://www.patreon.com/feross" 1403 | }, 1404 | { 1405 | "type": "consulting", 1406 | "url": "https://feross.org/support" 1407 | } 1408 | ], 1409 | "dependencies": { 1410 | "queue-microtask": "^1.2.2" 1411 | } 1412 | }, 1413 | "node_modules/semver": { 1414 | "version": "7.3.5", 1415 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 1416 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 1417 | "dev": true, 1418 | "dependencies": { 1419 | "lru-cache": "^6.0.0" 1420 | }, 1421 | "bin": { 1422 | "semver": "bin/semver.js" 1423 | }, 1424 | "engines": { 1425 | "node": ">=10" 1426 | } 1427 | }, 1428 | "node_modules/shebang-command": { 1429 | "version": "2.0.0", 1430 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1431 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1432 | "dev": true, 1433 | "dependencies": { 1434 | "shebang-regex": "^3.0.0" 1435 | }, 1436 | "engines": { 1437 | "node": ">=8" 1438 | } 1439 | }, 1440 | "node_modules/shebang-regex": { 1441 | "version": "3.0.0", 1442 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1443 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1444 | "dev": true, 1445 | "engines": { 1446 | "node": ">=8" 1447 | } 1448 | }, 1449 | "node_modules/slash": { 1450 | "version": "3.0.0", 1451 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 1452 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 1453 | "dev": true, 1454 | "engines": { 1455 | "node": ">=8" 1456 | } 1457 | }, 1458 | "node_modules/split2": { 1459 | "version": "4.1.0", 1460 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", 1461 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", 1462 | "engines": { 1463 | "node": ">= 10.x" 1464 | } 1465 | }, 1466 | "node_modules/strip-ansi": { 1467 | "version": "6.0.1", 1468 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1469 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1470 | "dev": true, 1471 | "dependencies": { 1472 | "ansi-regex": "^5.0.1" 1473 | }, 1474 | "engines": { 1475 | "node": ">=8" 1476 | } 1477 | }, 1478 | "node_modules/strip-json-comments": { 1479 | "version": "3.1.1", 1480 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1481 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1482 | "dev": true, 1483 | "engines": { 1484 | "node": ">=8" 1485 | }, 1486 | "funding": { 1487 | "url": "https://github.com/sponsors/sindresorhus" 1488 | } 1489 | }, 1490 | "node_modules/supports-color": { 1491 | "version": "7.2.0", 1492 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1493 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1494 | "dev": true, 1495 | "dependencies": { 1496 | "has-flag": "^4.0.0" 1497 | }, 1498 | "engines": { 1499 | "node": ">=8" 1500 | } 1501 | }, 1502 | "node_modules/text-table": { 1503 | "version": "0.2.0", 1504 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1505 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1506 | "dev": true 1507 | }, 1508 | "node_modules/to-regex-range": { 1509 | "version": "5.0.1", 1510 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1511 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1512 | "dev": true, 1513 | "dependencies": { 1514 | "is-number": "^7.0.0" 1515 | }, 1516 | "engines": { 1517 | "node": ">=8.0" 1518 | } 1519 | }, 1520 | "node_modules/tslib": { 1521 | "version": "1.14.1", 1522 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 1523 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 1524 | "dev": true 1525 | }, 1526 | "node_modules/tsutils": { 1527 | "version": "3.21.0", 1528 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", 1529 | "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", 1530 | "dev": true, 1531 | "dependencies": { 1532 | "tslib": "^1.8.1" 1533 | }, 1534 | "engines": { 1535 | "node": ">= 6" 1536 | }, 1537 | "peerDependencies": { 1538 | "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" 1539 | } 1540 | }, 1541 | "node_modules/type-check": { 1542 | "version": "0.4.0", 1543 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1544 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1545 | "dev": true, 1546 | "dependencies": { 1547 | "prelude-ls": "^1.2.1" 1548 | }, 1549 | "engines": { 1550 | "node": ">= 0.8.0" 1551 | } 1552 | }, 1553 | "node_modules/type-fest": { 1554 | "version": "0.20.2", 1555 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 1556 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 1557 | "dev": true, 1558 | "engines": { 1559 | "node": ">=10" 1560 | }, 1561 | "funding": { 1562 | "url": "https://github.com/sponsors/sindresorhus" 1563 | } 1564 | }, 1565 | "node_modules/typescript": { 1566 | "version": "4.5.5", 1567 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", 1568 | "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", 1569 | "dev": true, 1570 | "bin": { 1571 | "tsc": "bin/tsc", 1572 | "tsserver": "bin/tsserver" 1573 | }, 1574 | "engines": { 1575 | "node": ">=4.2.0" 1576 | } 1577 | }, 1578 | "node_modules/uri-js": { 1579 | "version": "4.4.1", 1580 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1581 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1582 | "dev": true, 1583 | "dependencies": { 1584 | "punycode": "^2.1.0" 1585 | } 1586 | }, 1587 | "node_modules/v8-compile-cache": { 1588 | "version": "2.3.0", 1589 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 1590 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 1591 | "dev": true 1592 | }, 1593 | "node_modules/which": { 1594 | "version": "2.0.2", 1595 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1596 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1597 | "dev": true, 1598 | "dependencies": { 1599 | "isexe": "^2.0.0" 1600 | }, 1601 | "bin": { 1602 | "node-which": "bin/node-which" 1603 | }, 1604 | "engines": { 1605 | "node": ">= 8" 1606 | } 1607 | }, 1608 | "node_modules/word-wrap": { 1609 | "version": "1.2.3", 1610 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 1611 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 1612 | "dev": true, 1613 | "engines": { 1614 | "node": ">=0.10.0" 1615 | } 1616 | }, 1617 | "node_modules/wrappy": { 1618 | "version": "1.0.2", 1619 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1620 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1621 | "dev": true 1622 | }, 1623 | "node_modules/xtend": { 1624 | "version": "4.0.2", 1625 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1626 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 1627 | "engines": { 1628 | "node": ">=0.4" 1629 | } 1630 | }, 1631 | "node_modules/yallist": { 1632 | "version": "4.0.0", 1633 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1634 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 1635 | "dev": true 1636 | } 1637 | }, 1638 | "dependencies": { 1639 | "@eslint/eslintrc": { 1640 | "version": "1.0.5", 1641 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", 1642 | "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", 1643 | "dev": true, 1644 | "requires": { 1645 | "ajv": "^6.12.4", 1646 | "debug": "^4.3.2", 1647 | "espree": "^9.2.0", 1648 | "globals": "^13.9.0", 1649 | "ignore": "^4.0.6", 1650 | "import-fresh": "^3.2.1", 1651 | "js-yaml": "^4.1.0", 1652 | "minimatch": "^3.0.4", 1653 | "strip-json-comments": "^3.1.1" 1654 | }, 1655 | "dependencies": { 1656 | "ignore": { 1657 | "version": "4.0.6", 1658 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 1659 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 1660 | "dev": true 1661 | } 1662 | } 1663 | }, 1664 | "@humanwhocodes/config-array": { 1665 | "version": "0.9.3", 1666 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", 1667 | "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", 1668 | "dev": true, 1669 | "requires": { 1670 | "@humanwhocodes/object-schema": "^1.2.1", 1671 | "debug": "^4.1.1", 1672 | "minimatch": "^3.0.4" 1673 | } 1674 | }, 1675 | "@humanwhocodes/object-schema": { 1676 | "version": "1.2.1", 1677 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 1678 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 1679 | "dev": true 1680 | }, 1681 | "@nodelib/fs.scandir": { 1682 | "version": "2.1.5", 1683 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 1684 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 1685 | "dev": true, 1686 | "requires": { 1687 | "@nodelib/fs.stat": "2.0.5", 1688 | "run-parallel": "^1.1.9" 1689 | } 1690 | }, 1691 | "@nodelib/fs.stat": { 1692 | "version": "2.0.5", 1693 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 1694 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 1695 | "dev": true 1696 | }, 1697 | "@nodelib/fs.walk": { 1698 | "version": "1.2.8", 1699 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 1700 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 1701 | "dev": true, 1702 | "requires": { 1703 | "@nodelib/fs.scandir": "2.1.5", 1704 | "fastq": "^1.6.0" 1705 | } 1706 | }, 1707 | "@types/json-schema": { 1708 | "version": "7.0.9", 1709 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", 1710 | "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", 1711 | "dev": true 1712 | }, 1713 | "@types/node": { 1714 | "version": "17.0.14", 1715 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz", 1716 | "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==" 1717 | }, 1718 | "@types/pg": { 1719 | "version": "8.6.4", 1720 | "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.4.tgz", 1721 | "integrity": "sha512-uYA7UMVzDFpJobCrqwW/iWkFmvizy6knIUgr0Quaw7K1Le3ZnF7hI3bKqFoxPZ+fju1Sc7zdTvOl9YfFZPcmeA==", 1722 | "requires": { 1723 | "@types/node": "*", 1724 | "pg-protocol": "*", 1725 | "pg-types": "^2.2.0" 1726 | } 1727 | }, 1728 | "@typescript-eslint/eslint-plugin": { 1729 | "version": "5.10.2", 1730 | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.2.tgz", 1731 | "integrity": "sha512-4W/9lLuE+v27O/oe7hXJKjNtBLnZE8tQAFpapdxwSVHqtmIoPB1gph3+ahNwVuNL37BX7YQHyGF9Xv6XCnIX2Q==", 1732 | "dev": true, 1733 | "requires": { 1734 | "@typescript-eslint/scope-manager": "5.10.2", 1735 | "@typescript-eslint/type-utils": "5.10.2", 1736 | "@typescript-eslint/utils": "5.10.2", 1737 | "debug": "^4.3.2", 1738 | "functional-red-black-tree": "^1.0.1", 1739 | "ignore": "^5.1.8", 1740 | "regexpp": "^3.2.0", 1741 | "semver": "^7.3.5", 1742 | "tsutils": "^3.21.0" 1743 | } 1744 | }, 1745 | "@typescript-eslint/parser": { 1746 | "version": "5.10.2", 1747 | "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.2.tgz", 1748 | "integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==", 1749 | "dev": true, 1750 | "requires": { 1751 | "@typescript-eslint/scope-manager": "5.10.2", 1752 | "@typescript-eslint/types": "5.10.2", 1753 | "@typescript-eslint/typescript-estree": "5.10.2", 1754 | "debug": "^4.3.2" 1755 | } 1756 | }, 1757 | "@typescript-eslint/scope-manager": { 1758 | "version": "5.10.2", 1759 | "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", 1760 | "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", 1761 | "dev": true, 1762 | "requires": { 1763 | "@typescript-eslint/types": "5.10.2", 1764 | "@typescript-eslint/visitor-keys": "5.10.2" 1765 | } 1766 | }, 1767 | "@typescript-eslint/type-utils": { 1768 | "version": "5.10.2", 1769 | "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.2.tgz", 1770 | "integrity": "sha512-uRKSvw/Ccs5FYEoXW04Z5VfzF2iiZcx8Fu7DGIB7RHozuP0VbKNzP1KfZkHBTM75pCpsWxIthEH1B33dmGBKHw==", 1771 | "dev": true, 1772 | "requires": { 1773 | "@typescript-eslint/utils": "5.10.2", 1774 | "debug": "^4.3.2", 1775 | "tsutils": "^3.21.0" 1776 | } 1777 | }, 1778 | "@typescript-eslint/types": { 1779 | "version": "5.10.2", 1780 | "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", 1781 | "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", 1782 | "dev": true 1783 | }, 1784 | "@typescript-eslint/typescript-estree": { 1785 | "version": "5.10.2", 1786 | "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", 1787 | "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", 1788 | "dev": true, 1789 | "requires": { 1790 | "@typescript-eslint/types": "5.10.2", 1791 | "@typescript-eslint/visitor-keys": "5.10.2", 1792 | "debug": "^4.3.2", 1793 | "globby": "^11.0.4", 1794 | "is-glob": "^4.0.3", 1795 | "semver": "^7.3.5", 1796 | "tsutils": "^3.21.0" 1797 | } 1798 | }, 1799 | "@typescript-eslint/utils": { 1800 | "version": "5.10.2", 1801 | "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", 1802 | "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", 1803 | "dev": true, 1804 | "requires": { 1805 | "@types/json-schema": "^7.0.9", 1806 | "@typescript-eslint/scope-manager": "5.10.2", 1807 | "@typescript-eslint/types": "5.10.2", 1808 | "@typescript-eslint/typescript-estree": "5.10.2", 1809 | "eslint-scope": "^5.1.1", 1810 | "eslint-utils": "^3.0.0" 1811 | } 1812 | }, 1813 | "@typescript-eslint/visitor-keys": { 1814 | "version": "5.10.2", 1815 | "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", 1816 | "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", 1817 | "dev": true, 1818 | "requires": { 1819 | "@typescript-eslint/types": "5.10.2", 1820 | "eslint-visitor-keys": "^3.0.0" 1821 | } 1822 | }, 1823 | "acorn": { 1824 | "version": "8.7.0", 1825 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", 1826 | "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", 1827 | "dev": true 1828 | }, 1829 | "acorn-jsx": { 1830 | "version": "5.3.2", 1831 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1832 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1833 | "dev": true, 1834 | "requires": {} 1835 | }, 1836 | "ajv": { 1837 | "version": "6.12.6", 1838 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1839 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1840 | "dev": true, 1841 | "requires": { 1842 | "fast-deep-equal": "^3.1.1", 1843 | "fast-json-stable-stringify": "^2.0.0", 1844 | "json-schema-traverse": "^0.4.1", 1845 | "uri-js": "^4.2.2" 1846 | } 1847 | }, 1848 | "ansi-regex": { 1849 | "version": "5.0.1", 1850 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1851 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1852 | "dev": true 1853 | }, 1854 | "ansi-styles": { 1855 | "version": "4.3.0", 1856 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1857 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1858 | "dev": true, 1859 | "requires": { 1860 | "color-convert": "^2.0.1" 1861 | } 1862 | }, 1863 | "argparse": { 1864 | "version": "2.0.1", 1865 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1866 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1867 | "dev": true 1868 | }, 1869 | "array-union": { 1870 | "version": "2.1.0", 1871 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", 1872 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", 1873 | "dev": true 1874 | }, 1875 | "balanced-match": { 1876 | "version": "1.0.2", 1877 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1878 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1879 | "dev": true 1880 | }, 1881 | "brace-expansion": { 1882 | "version": "1.1.11", 1883 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1884 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1885 | "dev": true, 1886 | "requires": { 1887 | "balanced-match": "^1.0.0", 1888 | "concat-map": "0.0.1" 1889 | } 1890 | }, 1891 | "braces": { 1892 | "version": "3.0.2", 1893 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1894 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1895 | "dev": true, 1896 | "requires": { 1897 | "fill-range": "^7.0.1" 1898 | } 1899 | }, 1900 | "buffer-writer": { 1901 | "version": "2.0.0", 1902 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 1903 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 1904 | }, 1905 | "callsites": { 1906 | "version": "3.1.0", 1907 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1908 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1909 | "dev": true 1910 | }, 1911 | "chalk": { 1912 | "version": "4.1.2", 1913 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1914 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1915 | "dev": true, 1916 | "requires": { 1917 | "ansi-styles": "^4.1.0", 1918 | "supports-color": "^7.1.0" 1919 | } 1920 | }, 1921 | "color-convert": { 1922 | "version": "2.0.1", 1923 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1924 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1925 | "dev": true, 1926 | "requires": { 1927 | "color-name": "~1.1.4" 1928 | } 1929 | }, 1930 | "color-name": { 1931 | "version": "1.1.4", 1932 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1933 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1934 | "dev": true 1935 | }, 1936 | "concat-map": { 1937 | "version": "0.0.1", 1938 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1939 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 1940 | "dev": true 1941 | }, 1942 | "cross-spawn": { 1943 | "version": "7.0.3", 1944 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 1945 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 1946 | "dev": true, 1947 | "requires": { 1948 | "path-key": "^3.1.0", 1949 | "shebang-command": "^2.0.0", 1950 | "which": "^2.0.1" 1951 | } 1952 | }, 1953 | "debug": { 1954 | "version": "4.3.3", 1955 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 1956 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 1957 | "dev": true, 1958 | "requires": { 1959 | "ms": "2.1.2" 1960 | } 1961 | }, 1962 | "deep-is": { 1963 | "version": "0.1.4", 1964 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 1965 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 1966 | "dev": true 1967 | }, 1968 | "dir-glob": { 1969 | "version": "3.0.1", 1970 | "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", 1971 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", 1972 | "dev": true, 1973 | "requires": { 1974 | "path-type": "^4.0.0" 1975 | } 1976 | }, 1977 | "doctrine": { 1978 | "version": "3.0.0", 1979 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 1980 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 1981 | "dev": true, 1982 | "requires": { 1983 | "esutils": "^2.0.2" 1984 | } 1985 | }, 1986 | "escape-string-regexp": { 1987 | "version": "4.0.0", 1988 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1989 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1990 | "dev": true 1991 | }, 1992 | "eslint": { 1993 | "version": "8.8.0", 1994 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", 1995 | "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", 1996 | "dev": true, 1997 | "requires": { 1998 | "@eslint/eslintrc": "^1.0.5", 1999 | "@humanwhocodes/config-array": "^0.9.2", 2000 | "ajv": "^6.10.0", 2001 | "chalk": "^4.0.0", 2002 | "cross-spawn": "^7.0.2", 2003 | "debug": "^4.3.2", 2004 | "doctrine": "^3.0.0", 2005 | "escape-string-regexp": "^4.0.0", 2006 | "eslint-scope": "^7.1.0", 2007 | "eslint-utils": "^3.0.0", 2008 | "eslint-visitor-keys": "^3.2.0", 2009 | "espree": "^9.3.0", 2010 | "esquery": "^1.4.0", 2011 | "esutils": "^2.0.2", 2012 | "fast-deep-equal": "^3.1.3", 2013 | "file-entry-cache": "^6.0.1", 2014 | "functional-red-black-tree": "^1.0.1", 2015 | "glob-parent": "^6.0.1", 2016 | "globals": "^13.6.0", 2017 | "ignore": "^5.2.0", 2018 | "import-fresh": "^3.0.0", 2019 | "imurmurhash": "^0.1.4", 2020 | "is-glob": "^4.0.0", 2021 | "js-yaml": "^4.1.0", 2022 | "json-stable-stringify-without-jsonify": "^1.0.1", 2023 | "levn": "^0.4.1", 2024 | "lodash.merge": "^4.6.2", 2025 | "minimatch": "^3.0.4", 2026 | "natural-compare": "^1.4.0", 2027 | "optionator": "^0.9.1", 2028 | "regexpp": "^3.2.0", 2029 | "strip-ansi": "^6.0.1", 2030 | "strip-json-comments": "^3.1.0", 2031 | "text-table": "^0.2.0", 2032 | "v8-compile-cache": "^2.0.3" 2033 | }, 2034 | "dependencies": { 2035 | "eslint-scope": { 2036 | "version": "7.1.0", 2037 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", 2038 | "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", 2039 | "dev": true, 2040 | "requires": { 2041 | "esrecurse": "^4.3.0", 2042 | "estraverse": "^5.2.0" 2043 | } 2044 | }, 2045 | "estraverse": { 2046 | "version": "5.3.0", 2047 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2048 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2049 | "dev": true 2050 | } 2051 | } 2052 | }, 2053 | "eslint-scope": { 2054 | "version": "5.1.1", 2055 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", 2056 | "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", 2057 | "dev": true, 2058 | "requires": { 2059 | "esrecurse": "^4.3.0", 2060 | "estraverse": "^4.1.1" 2061 | } 2062 | }, 2063 | "eslint-utils": { 2064 | "version": "3.0.0", 2065 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", 2066 | "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", 2067 | "dev": true, 2068 | "requires": { 2069 | "eslint-visitor-keys": "^2.0.0" 2070 | }, 2071 | "dependencies": { 2072 | "eslint-visitor-keys": { 2073 | "version": "2.1.0", 2074 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", 2075 | "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", 2076 | "dev": true 2077 | } 2078 | } 2079 | }, 2080 | "eslint-visitor-keys": { 2081 | "version": "3.2.0", 2082 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", 2083 | "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", 2084 | "dev": true 2085 | }, 2086 | "espree": { 2087 | "version": "9.3.0", 2088 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", 2089 | "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", 2090 | "dev": true, 2091 | "requires": { 2092 | "acorn": "^8.7.0", 2093 | "acorn-jsx": "^5.3.1", 2094 | "eslint-visitor-keys": "^3.1.0" 2095 | } 2096 | }, 2097 | "esquery": { 2098 | "version": "1.4.0", 2099 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", 2100 | "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", 2101 | "dev": true, 2102 | "requires": { 2103 | "estraverse": "^5.1.0" 2104 | }, 2105 | "dependencies": { 2106 | "estraverse": { 2107 | "version": "5.3.0", 2108 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2109 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2110 | "dev": true 2111 | } 2112 | } 2113 | }, 2114 | "esrecurse": { 2115 | "version": "4.3.0", 2116 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2117 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2118 | "dev": true, 2119 | "requires": { 2120 | "estraverse": "^5.2.0" 2121 | }, 2122 | "dependencies": { 2123 | "estraverse": { 2124 | "version": "5.3.0", 2125 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2126 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2127 | "dev": true 2128 | } 2129 | } 2130 | }, 2131 | "estraverse": { 2132 | "version": "4.3.0", 2133 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 2134 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 2135 | "dev": true 2136 | }, 2137 | "esutils": { 2138 | "version": "2.0.3", 2139 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2140 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2141 | "dev": true 2142 | }, 2143 | "fast-deep-equal": { 2144 | "version": "3.1.3", 2145 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2146 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2147 | "dev": true 2148 | }, 2149 | "fast-glob": { 2150 | "version": "3.2.11", 2151 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", 2152 | "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", 2153 | "dev": true, 2154 | "requires": { 2155 | "@nodelib/fs.stat": "^2.0.2", 2156 | "@nodelib/fs.walk": "^1.2.3", 2157 | "glob-parent": "^5.1.2", 2158 | "merge2": "^1.3.0", 2159 | "micromatch": "^4.0.4" 2160 | }, 2161 | "dependencies": { 2162 | "glob-parent": { 2163 | "version": "5.1.2", 2164 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 2165 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 2166 | "dev": true, 2167 | "requires": { 2168 | "is-glob": "^4.0.1" 2169 | } 2170 | } 2171 | } 2172 | }, 2173 | "fast-json-stable-stringify": { 2174 | "version": "2.1.0", 2175 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2176 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2177 | "dev": true 2178 | }, 2179 | "fast-levenshtein": { 2180 | "version": "2.0.6", 2181 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2182 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 2183 | "dev": true 2184 | }, 2185 | "fastq": { 2186 | "version": "1.13.0", 2187 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 2188 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 2189 | "dev": true, 2190 | "requires": { 2191 | "reusify": "^1.0.4" 2192 | } 2193 | }, 2194 | "file-entry-cache": { 2195 | "version": "6.0.1", 2196 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 2197 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 2198 | "dev": true, 2199 | "requires": { 2200 | "flat-cache": "^3.0.4" 2201 | } 2202 | }, 2203 | "fill-range": { 2204 | "version": "7.0.1", 2205 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 2206 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 2207 | "dev": true, 2208 | "requires": { 2209 | "to-regex-range": "^5.0.1" 2210 | } 2211 | }, 2212 | "flat-cache": { 2213 | "version": "3.0.4", 2214 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 2215 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 2216 | "dev": true, 2217 | "requires": { 2218 | "flatted": "^3.1.0", 2219 | "rimraf": "^3.0.2" 2220 | } 2221 | }, 2222 | "flatted": { 2223 | "version": "3.2.5", 2224 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", 2225 | "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", 2226 | "dev": true 2227 | }, 2228 | "fs.realpath": { 2229 | "version": "1.0.0", 2230 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 2231 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 2232 | "dev": true 2233 | }, 2234 | "functional-red-black-tree": { 2235 | "version": "1.0.1", 2236 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 2237 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 2238 | "dev": true 2239 | }, 2240 | "glob": { 2241 | "version": "7.2.0", 2242 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 2243 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 2244 | "dev": true, 2245 | "requires": { 2246 | "fs.realpath": "^1.0.0", 2247 | "inflight": "^1.0.4", 2248 | "inherits": "2", 2249 | "minimatch": "^3.0.4", 2250 | "once": "^1.3.0", 2251 | "path-is-absolute": "^1.0.0" 2252 | } 2253 | }, 2254 | "glob-parent": { 2255 | "version": "6.0.2", 2256 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2257 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2258 | "dev": true, 2259 | "requires": { 2260 | "is-glob": "^4.0.3" 2261 | } 2262 | }, 2263 | "globals": { 2264 | "version": "13.12.1", 2265 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", 2266 | "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", 2267 | "dev": true, 2268 | "requires": { 2269 | "type-fest": "^0.20.2" 2270 | } 2271 | }, 2272 | "globby": { 2273 | "version": "11.1.0", 2274 | "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", 2275 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", 2276 | "dev": true, 2277 | "requires": { 2278 | "array-union": "^2.1.0", 2279 | "dir-glob": "^3.0.1", 2280 | "fast-glob": "^3.2.9", 2281 | "ignore": "^5.2.0", 2282 | "merge2": "^1.4.1", 2283 | "slash": "^3.0.0" 2284 | } 2285 | }, 2286 | "has-flag": { 2287 | "version": "4.0.0", 2288 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2289 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2290 | "dev": true 2291 | }, 2292 | "ignore": { 2293 | "version": "5.2.0", 2294 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", 2295 | "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", 2296 | "dev": true 2297 | }, 2298 | "import-fresh": { 2299 | "version": "3.3.0", 2300 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 2301 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 2302 | "dev": true, 2303 | "requires": { 2304 | "parent-module": "^1.0.0", 2305 | "resolve-from": "^4.0.0" 2306 | } 2307 | }, 2308 | "imurmurhash": { 2309 | "version": "0.1.4", 2310 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2311 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 2312 | "dev": true 2313 | }, 2314 | "inflight": { 2315 | "version": "1.0.6", 2316 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 2317 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 2318 | "dev": true, 2319 | "requires": { 2320 | "once": "^1.3.0", 2321 | "wrappy": "1" 2322 | } 2323 | }, 2324 | "inherits": { 2325 | "version": "2.0.4", 2326 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 2327 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 2328 | "dev": true 2329 | }, 2330 | "is-extglob": { 2331 | "version": "2.1.1", 2332 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 2333 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 2334 | "dev": true 2335 | }, 2336 | "is-glob": { 2337 | "version": "4.0.3", 2338 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 2339 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 2340 | "dev": true, 2341 | "requires": { 2342 | "is-extglob": "^2.1.1" 2343 | } 2344 | }, 2345 | "is-number": { 2346 | "version": "7.0.0", 2347 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 2348 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 2349 | "dev": true 2350 | }, 2351 | "isexe": { 2352 | "version": "2.0.0", 2353 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 2354 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 2355 | "dev": true 2356 | }, 2357 | "js-yaml": { 2358 | "version": "4.1.0", 2359 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 2360 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 2361 | "dev": true, 2362 | "requires": { 2363 | "argparse": "^2.0.1" 2364 | } 2365 | }, 2366 | "json-schema-traverse": { 2367 | "version": "0.4.1", 2368 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 2369 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 2370 | "dev": true 2371 | }, 2372 | "json-stable-stringify-without-jsonify": { 2373 | "version": "1.0.1", 2374 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 2375 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 2376 | "dev": true 2377 | }, 2378 | "levn": { 2379 | "version": "0.4.1", 2380 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 2381 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 2382 | "dev": true, 2383 | "requires": { 2384 | "prelude-ls": "^1.2.1", 2385 | "type-check": "~0.4.0" 2386 | } 2387 | }, 2388 | "lodash.merge": { 2389 | "version": "4.6.2", 2390 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 2391 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 2392 | "dev": true 2393 | }, 2394 | "lru-cache": { 2395 | "version": "6.0.0", 2396 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 2397 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 2398 | "dev": true, 2399 | "requires": { 2400 | "yallist": "^4.0.0" 2401 | } 2402 | }, 2403 | "merge2": { 2404 | "version": "1.4.1", 2405 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 2406 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 2407 | "dev": true 2408 | }, 2409 | "micromatch": { 2410 | "version": "4.0.4", 2411 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 2412 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 2413 | "dev": true, 2414 | "requires": { 2415 | "braces": "^3.0.1", 2416 | "picomatch": "^2.2.3" 2417 | } 2418 | }, 2419 | "minimatch": { 2420 | "version": "3.0.4", 2421 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 2422 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 2423 | "dev": true, 2424 | "requires": { 2425 | "brace-expansion": "^1.1.7" 2426 | } 2427 | }, 2428 | "ms": { 2429 | "version": "2.1.2", 2430 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 2431 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 2432 | "dev": true 2433 | }, 2434 | "natural-compare": { 2435 | "version": "1.4.0", 2436 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2437 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 2438 | "dev": true 2439 | }, 2440 | "once": { 2441 | "version": "1.4.0", 2442 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2443 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 2444 | "dev": true, 2445 | "requires": { 2446 | "wrappy": "1" 2447 | } 2448 | }, 2449 | "optionator": { 2450 | "version": "0.9.1", 2451 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 2452 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 2453 | "dev": true, 2454 | "requires": { 2455 | "deep-is": "^0.1.3", 2456 | "fast-levenshtein": "^2.0.6", 2457 | "levn": "^0.4.1", 2458 | "prelude-ls": "^1.2.1", 2459 | "type-check": "^0.4.0", 2460 | "word-wrap": "^1.2.3" 2461 | } 2462 | }, 2463 | "packet-reader": { 2464 | "version": "1.0.0", 2465 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 2466 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 2467 | }, 2468 | "parent-module": { 2469 | "version": "1.0.1", 2470 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2471 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2472 | "dev": true, 2473 | "requires": { 2474 | "callsites": "^3.0.0" 2475 | } 2476 | }, 2477 | "path-is-absolute": { 2478 | "version": "1.0.1", 2479 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 2480 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 2481 | "dev": true 2482 | }, 2483 | "path-key": { 2484 | "version": "3.1.1", 2485 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2486 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2487 | "dev": true 2488 | }, 2489 | "path-type": { 2490 | "version": "4.0.0", 2491 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", 2492 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", 2493 | "dev": true 2494 | }, 2495 | "pg": { 2496 | "version": "8.7.1", 2497 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", 2498 | "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", 2499 | "requires": { 2500 | "buffer-writer": "2.0.0", 2501 | "packet-reader": "1.0.0", 2502 | "pg-connection-string": "^2.5.0", 2503 | "pg-pool": "^3.4.1", 2504 | "pg-protocol": "^1.5.0", 2505 | "pg-types": "^2.1.0", 2506 | "pgpass": "1.x" 2507 | } 2508 | }, 2509 | "pg-connection-string": { 2510 | "version": "2.5.0", 2511 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", 2512 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" 2513 | }, 2514 | "pg-int8": { 2515 | "version": "1.0.1", 2516 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 2517 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 2518 | }, 2519 | "pg-pool": { 2520 | "version": "3.4.1", 2521 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", 2522 | "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", 2523 | "requires": {} 2524 | }, 2525 | "pg-protocol": { 2526 | "version": "1.5.0", 2527 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", 2528 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" 2529 | }, 2530 | "pg-types": { 2531 | "version": "2.2.0", 2532 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 2533 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 2534 | "requires": { 2535 | "pg-int8": "1.0.1", 2536 | "postgres-array": "~2.0.0", 2537 | "postgres-bytea": "~1.0.0", 2538 | "postgres-date": "~1.0.4", 2539 | "postgres-interval": "^1.1.0" 2540 | } 2541 | }, 2542 | "pgpass": { 2543 | "version": "1.0.5", 2544 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", 2545 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", 2546 | "requires": { 2547 | "split2": "^4.1.0" 2548 | } 2549 | }, 2550 | "picomatch": { 2551 | "version": "2.3.1", 2552 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 2553 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 2554 | "dev": true 2555 | }, 2556 | "postgres-array": { 2557 | "version": "2.0.0", 2558 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 2559 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 2560 | }, 2561 | "postgres-bytea": { 2562 | "version": "1.0.0", 2563 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 2564 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 2565 | }, 2566 | "postgres-date": { 2567 | "version": "1.0.7", 2568 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 2569 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" 2570 | }, 2571 | "postgres-interval": { 2572 | "version": "1.2.0", 2573 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 2574 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 2575 | "requires": { 2576 | "xtend": "^4.0.0" 2577 | } 2578 | }, 2579 | "prelude-ls": { 2580 | "version": "1.2.1", 2581 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2582 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2583 | "dev": true 2584 | }, 2585 | "punycode": { 2586 | "version": "2.1.1", 2587 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 2588 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 2589 | "dev": true 2590 | }, 2591 | "queue-microtask": { 2592 | "version": "1.2.3", 2593 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 2594 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 2595 | "dev": true 2596 | }, 2597 | "regexpp": { 2598 | "version": "3.2.0", 2599 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", 2600 | "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", 2601 | "dev": true 2602 | }, 2603 | "resolve-from": { 2604 | "version": "4.0.0", 2605 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2606 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2607 | "dev": true 2608 | }, 2609 | "reusify": { 2610 | "version": "1.0.4", 2611 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 2612 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 2613 | "dev": true 2614 | }, 2615 | "rimraf": { 2616 | "version": "3.0.2", 2617 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 2618 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 2619 | "dev": true, 2620 | "requires": { 2621 | "glob": "^7.1.3" 2622 | } 2623 | }, 2624 | "run-parallel": { 2625 | "version": "1.2.0", 2626 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 2627 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 2628 | "dev": true, 2629 | "requires": { 2630 | "queue-microtask": "^1.2.2" 2631 | } 2632 | }, 2633 | "semver": { 2634 | "version": "7.3.5", 2635 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 2636 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 2637 | "dev": true, 2638 | "requires": { 2639 | "lru-cache": "^6.0.0" 2640 | } 2641 | }, 2642 | "shebang-command": { 2643 | "version": "2.0.0", 2644 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2645 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2646 | "dev": true, 2647 | "requires": { 2648 | "shebang-regex": "^3.0.0" 2649 | } 2650 | }, 2651 | "shebang-regex": { 2652 | "version": "3.0.0", 2653 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2654 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2655 | "dev": true 2656 | }, 2657 | "slash": { 2658 | "version": "3.0.0", 2659 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 2660 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 2661 | "dev": true 2662 | }, 2663 | "split2": { 2664 | "version": "4.1.0", 2665 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", 2666 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" 2667 | }, 2668 | "strip-ansi": { 2669 | "version": "6.0.1", 2670 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 2671 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 2672 | "dev": true, 2673 | "requires": { 2674 | "ansi-regex": "^5.0.1" 2675 | } 2676 | }, 2677 | "strip-json-comments": { 2678 | "version": "3.1.1", 2679 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 2680 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 2681 | "dev": true 2682 | }, 2683 | "supports-color": { 2684 | "version": "7.2.0", 2685 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2686 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2687 | "dev": true, 2688 | "requires": { 2689 | "has-flag": "^4.0.0" 2690 | } 2691 | }, 2692 | "text-table": { 2693 | "version": "0.2.0", 2694 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 2695 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 2696 | "dev": true 2697 | }, 2698 | "to-regex-range": { 2699 | "version": "5.0.1", 2700 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 2701 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 2702 | "dev": true, 2703 | "requires": { 2704 | "is-number": "^7.0.0" 2705 | } 2706 | }, 2707 | "tslib": { 2708 | "version": "1.14.1", 2709 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 2710 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 2711 | "dev": true 2712 | }, 2713 | "tsutils": { 2714 | "version": "3.21.0", 2715 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", 2716 | "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", 2717 | "dev": true, 2718 | "requires": { 2719 | "tslib": "^1.8.1" 2720 | } 2721 | }, 2722 | "type-check": { 2723 | "version": "0.4.0", 2724 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2725 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2726 | "dev": true, 2727 | "requires": { 2728 | "prelude-ls": "^1.2.1" 2729 | } 2730 | }, 2731 | "type-fest": { 2732 | "version": "0.20.2", 2733 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 2734 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 2735 | "dev": true 2736 | }, 2737 | "typescript": { 2738 | "version": "4.5.5", 2739 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", 2740 | "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", 2741 | "dev": true 2742 | }, 2743 | "uri-js": { 2744 | "version": "4.4.1", 2745 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 2746 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 2747 | "dev": true, 2748 | "requires": { 2749 | "punycode": "^2.1.0" 2750 | } 2751 | }, 2752 | "v8-compile-cache": { 2753 | "version": "2.3.0", 2754 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 2755 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 2756 | "dev": true 2757 | }, 2758 | "which": { 2759 | "version": "2.0.2", 2760 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2761 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2762 | "dev": true, 2763 | "requires": { 2764 | "isexe": "^2.0.0" 2765 | } 2766 | }, 2767 | "word-wrap": { 2768 | "version": "1.2.3", 2769 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 2770 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 2771 | "dev": true 2772 | }, 2773 | "wrappy": { 2774 | "version": "1.0.2", 2775 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2776 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 2777 | "dev": true 2778 | }, 2779 | "xtend": { 2780 | "version": "4.0.2", 2781 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 2782 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 2783 | }, 2784 | "yallist": { 2785 | "version": "4.0.0", 2786 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 2787 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 2788 | "dev": true 2789 | } 2790 | } 2791 | } 2792 | -------------------------------------------------------------------------------- /helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "start": "tsc && node dist/app.js", 8 | "lint": "eslint . --ext .ts", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^5.10.1", 16 | "@typescript-eslint/parser": "^5.10.1", 17 | "eslint": "^8.7.0", 18 | "typescript": "^4.5.5" 19 | }, 20 | "dependencies": { 21 | "@types/pg": "^8.6.4", 22 | "pg": "^8.7.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /helper/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "pg"; 2 | 3 | /** 4 | * Create Materialize sources and materialized views 5 | * Before creating the views it will check if they aren't created already. 6 | */ 7 | async function setUpMaterialize() { 8 | const pool = await new Pool({ 9 | host: "materialized", 10 | port: 6875, 11 | user: "materialize", 12 | password: "materialize", 13 | database: "materialize", 14 | }); 15 | const poolClient = await pool.connect(); 16 | 17 | await poolClient.query(` 18 | CREATE MATERIALIZED SOURCE IF NOT EXISTS antennas_publication_source 19 | FROM POSTGRES 20 | CONNECTION 'host=postgres port=5432 user=materialize password=materialize dbname=postgres' 21 | PUBLICATION 'antennas_publication_source'; 22 | `); 23 | 24 | const { rowCount } = await pool.query( 25 | "SELECT * FROM mz_views WHERE name='antennas' OR name='antennas_performance';" 26 | ); 27 | 28 | if (!rowCount) { 29 | await poolClient.query(` 30 | CREATE MATERIALIZED VIEWS FROM SOURCE antennas_publication_source; 31 | `); 32 | 33 | await poolClient.query(` 34 | CREATE MATERIALIZED VIEW IF NOT EXISTS last_half_minute_updates AS 35 | SELECT A.antenna_id, A.geojson, performance, AP.updated_at, ((CAST(EXTRACT( epoch from AP.updated_at) AS NUMERIC) * 1000) + 30000) 36 | FROM antennas A JOIN antennas_performance AP ON (A.antenna_id = AP.antenna_id) 37 | WHERE ((CAST(EXTRACT( epoch from AP.updated_at) AS NUMERIC) * 1000) + 30000) > mz_logical_timestamp(); 38 | `); 39 | 40 | await poolClient.query(` 41 | CREATE MATERIALIZED VIEW IF NOT EXISTS last_half_minute_performance_per_antenna AS 42 | SELECT antenna_id, geojson, AVG(performance) as performance 43 | FROM last_half_minute_updates 44 | GROUP BY antenna_id, geojson; 45 | `); 46 | } 47 | 48 | poolClient.release(); 49 | } 50 | 51 | /** 52 | * Build a custom Postgres insert with a random performance and clients connected 53 | * @param antennaId Antenna Identifier 54 | * @returns 55 | */ 56 | function buildQuery(antennaId: number) { 57 | return ` 58 | INSERT INTO antennas_performance (antenna_id, clients_connected, performance, updated_at) VALUES ( 59 | ${antennaId}, 60 | ${Math.ceil(Math.random() * 100)}, 61 | ${Math.random() * 10}, 62 | now() 63 | ); 64 | `; 65 | } 66 | 67 | /** 68 | * Generate data to Postgres indefinitely 69 | */ 70 | async function dataGenerator() { 71 | const pool = await new Pool({ 72 | host: "postgres", 73 | user: "postgres", 74 | password: "pg_password", 75 | }); 76 | 77 | const poolClient = await pool.connect(); 78 | setInterval(() => { 79 | const query = [1, 2, 3, 4, 5, 6, 7] 80 | .map((antennaId) => buildQuery(antennaId)) 81 | .join("\n"); 82 | 83 | poolClient.query(query); 84 | }, 1000); 85 | } 86 | 87 | setUpMaterialize() 88 | .then(() => { 89 | console.log("Generating data."); 90 | dataGenerator(); 91 | }) 92 | .catch((err) => { 93 | console.error(err); 94 | }); 95 | -------------------------------------------------------------------------------- /helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "lib": ["esnext.asynciterable"] 10 | }, 11 | "lib": ["es2015"] 12 | } 13 | -------------------------------------------------------------------------------- /microservice/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /microservice/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /microservice/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | 13 | # Bundle app source 14 | COPY . . 15 | 16 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /microservice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/app.js", 6 | "scripts": { 7 | "start": "tsc && node dist/app.js", 8 | "lint": "eslint . --ext .ts", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^5.10.1", 16 | "@typescript-eslint/parser": "^5.10.1", 17 | "eslint": "^8.7.0", 18 | "typescript": "^4.5.5" 19 | }, 20 | "dependencies": { 21 | "@apollo/client": "^3.5.9", 22 | "@types/pg": "^8.6.4", 23 | "@types/uuid": "^8.3.4", 24 | "apollo-link-http": "^1.5.17", 25 | "dom": "^0.0.3", 26 | "graphql": "^15.8.0", 27 | "graphql-ws": "^5.5.5", 28 | "node-fetch": "^2.6.7", 29 | "pg": "^8.7.1", 30 | "uuid": "^8.3.2", 31 | "ws": "^8.5.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /microservice/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "pg"; 2 | import { v4 } from "uuid"; 3 | import ws from "ws"; 4 | import { createClient } from "graphql-ws"; 5 | 6 | /** 7 | * Postgres Client 8 | */ 9 | const postgresPool = new Pool({ 10 | host: "postgres", 11 | // host: "localhost", 12 | port: 5432, 13 | user: "postgres", 14 | password: "pg_password", 15 | database: "postgres", 16 | }); 17 | 18 | /** 19 | * Backend client 20 | */ 21 | const graphqlClient = createClient({ 22 | url: "ws://backend:4000/graphql", 23 | // url: "ws://localhost:4000/graphql", 24 | webSocketImpl: ws, 25 | generateID: v4, 26 | on: { 27 | error: (error) => { 28 | console.log("Error: ", error); 29 | }, 30 | }, 31 | }); 32 | 33 | /** 34 | * Map to follow bad performance antennas 35 | */ 36 | const performanceMapCounter = new Map(); 37 | const alreadyImprovingSet = new Set(); 38 | 39 | /** 40 | * Build a custom Postgres insert with a random performance and clients connected 41 | * @param antennaId Antenna Identifier 42 | * @returns 43 | */ 44 | const buildQuery = (antennaId: number, value: number) => { 45 | return ` 46 | INSERT INTO antennas_performance (antenna_id, clients_connected, performance, updated_at) VALUES ( 47 | ${antennaId}, 48 | ${Math.ceil(Math.random() * 100)}, 49 | ${value + Math.random()}, 50 | now() 51 | ); 52 | `; 53 | }; 54 | 55 | /** 56 | * Find and enable helper antennas 57 | * @param antennaName 58 | */ 59 | const findHelperAntennas = ( 60 | antennaName 61 | ): Promise> => { 62 | const query = `SELECT antenna_id FROM antennas WHERE CAST(CAST(geojson as json)->>'properties' as json)->>'helps' = '${antennaName}';`; 63 | let helperAntennas = []; 64 | 65 | return new Promise((res) => { 66 | postgresPool.connect(async (err, postgresClient, done) => { 67 | if (err) { 68 | console.error(err); 69 | } else { 70 | try { 71 | const results = await postgresClient.query(query); 72 | helperAntennas = results.rows; 73 | } catch (clientErr) { 74 | console.error(clientErr); 75 | } finally { 76 | done(); 77 | res(helperAntennas); 78 | } 79 | } 80 | }); 81 | }); 82 | }; 83 | 84 | const improveAntennaPerformance = async (antennaId, antennaName) => { 85 | console.log("Improving performance for antenna: ", antennaId); 86 | alreadyImprovingSet.add(antennaId); 87 | 88 | const helperAntennas = await findHelperAntennas(antennaName); 89 | 90 | /** 91 | * Improve antenna performance 92 | */ 93 | postgresPool.connect((err, postgresClient, done) => { 94 | if (err) { 95 | console.error(err); 96 | } else { 97 | let count = 0; 98 | const intervalId = setInterval(async () => { 99 | const query = 100 | buildQuery(antennaId, 7.5) + 101 | "\n" + 102 | helperAntennas.map((x) => buildQuery(x.antenna_id, 5)).join("\n"); 103 | count += 1; 104 | try { 105 | await postgresClient.query(query); 106 | } catch (clientErr) { 107 | console.error(clientErr); 108 | } finally { 109 | /** 110 | * Clean set and interval 111 | */ 112 | if (count === 100) { 113 | console.log(`Stopping interval for ${antennaId}`); 114 | clearInterval(intervalId); 115 | alreadyImprovingSet.delete(antennaId); 116 | done(); 117 | } 118 | } 119 | }, 250); 120 | } 121 | }); 122 | }; 123 | 124 | /** 125 | * Listen antennas performance events 126 | */ 127 | const antennasPerformanceListener = (data) => { 128 | const { data: antennasData } = data; 129 | const { antennasUpdates } = antennasData; 130 | 131 | antennasUpdates.forEach((x) => { 132 | const { antenna_id: antennaId, geojson: rawGeoJson, performance } = x; 133 | 134 | try { 135 | const geojson = JSON.parse(rawGeoJson); 136 | const { properties } = geojson; 137 | const { name: antennaName } = properties; 138 | 139 | const antennaCounter = performanceMapCounter.get(antennaId); 140 | 141 | if (performance < 4.75) { 142 | if (antennaCounter > 7 && !alreadyImprovingSet.has(antennaId)) { 143 | improveAntennaPerformance(antennaId, antennaName); 144 | } else { 145 | performanceMapCounter.set( 146 | antennaId, 147 | typeof antennaCounter === "number" ? antennaCounter + 1 : 1 148 | ); 149 | } 150 | } else { 151 | performanceMapCounter.delete(antennaId); 152 | } 153 | } catch (errParsing) { 154 | console.error(errParsing); 155 | } 156 | }); 157 | }; 158 | 159 | const onError = (err) => { 160 | console.error("Ouch. Some error: ", err); 161 | }; 162 | 163 | const onComplete = () => { 164 | console.log("Finished."); 165 | }; 166 | 167 | graphqlClient.subscribe( 168 | { 169 | query: 170 | "subscription { antennasUpdates { antenna_id, geojson, performance } }", 171 | }, 172 | { 173 | next: antennasPerformanceListener, 174 | error: onError, 175 | complete: onComplete, 176 | } 177 | ); 178 | -------------------------------------------------------------------------------- /microservice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "lib": [ 10 | "esnext.asynciterable", 11 | "dom" 12 | ] 13 | }, 14 | "lib": [ 15 | "es2015", 16 | ], 17 | } -------------------------------------------------------------------------------- /postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres 2 | 3 | # Automatically runned by Postgres 4 | ADD ./create.sh /docker-entrypoint-initdb.d/ 5 | ADD ./seed.sh /docker-entrypoint-initdb.d/ 6 | 7 | # Create Tables, Publications and Roles 8 | ADD ./create.sql /scripts/ 9 | ADD ./seed.sql /scripts/ -------------------------------------------------------------------------------- /postgres/create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Creating tables, publications and roles" 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f /scripts/create.sql -------------------------------------------------------------------------------- /postgres/create.sql: -------------------------------------------------------------------------------- 1 | -- Antennas table will contain the identifier and geojson for each antenna. 2 | CREATE TABLE antennas ( 3 | antenna_id INT GENERATED ALWAYS AS IDENTITY, 4 | geojson JSON NOT NULL 5 | ); 6 | 7 | -- Antennas performance table will contain every performance update available 8 | CREATE TABLE antennas_performance ( 9 | antenna_id INT, 10 | clients_connected INT NOT NULL, 11 | performance INT NOT NULL, 12 | updated_at timestamp NOT NULL 13 | ); 14 | 15 | -- Enable REPLICA for both tables 16 | ALTER TABLE antennas REPLICA IDENTITY FULL; 17 | ALTER TABLE antennas_performance REPLICA IDENTITY FULL; 18 | 19 | -- Create publication on the created tables 20 | CREATE PUBLICATION antennas_publication_source FOR TABLE antennas, antennas_performance; 21 | 22 | -- Create user and role to be used by Materialize 23 | CREATE ROLE materialize REPLICATION LOGIN PASSWORD 'materialize'; 24 | GRANT SELECT ON antennas, antennas_performance TO materialize; -------------------------------------------------------------------------------- /postgres/rollback.sql: -------------------------------------------------------------------------------- 1 | ----- Rollback: 2 | DROP PUBLICATION antennas_publication_source; 3 | DROP TABLE antennas_performance; 4 | DROP TABLE antennas; 5 | DROP ROLE materialize; -------------------------------------------------------------------------------- /postgres/seed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Creating tables, publications and roles" 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f /scripts/seed.sql -------------------------------------------------------------------------------- /postgres/seed.sql: -------------------------------------------------------------------------------- 1 | ------ Seven Manhattan Antennas: 2 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9999, 40.7396] }, "properties": { "name": "W 15th St & 7th Ave" } }'); 3 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9849, 40.7454] }, "properties": { "name": "E 30th St & Madison Ave" } }'); 4 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9949, 40.7554] }, "properties": { "name": "W 37th St & 9th Ave" } }'); 5 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9985, 40.7266] }, "properties": { "name": "W Houston St & Wooster St" } }'); 6 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.981, 40.733] }, "properties": { "name": "1st Ave & E 17th St" } }'); 7 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9829, 40.7166] }, "properties": { "name": "Williamsburg Bridge & Pitt St" } }'); 8 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0075, 40.7126] }, "properties": { "name": "Broadway & Park Pl" } }'); 9 | 10 | ------ Helper Antennas 11 | -- W 15th St & 7th Ave (Helps Antenna ID: 1) 12 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9979, 40.7434] }, "properties": { "name": "W 21st St", "helps": "W 15th St & 7th Ave" } }'); 13 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0066, 40.7432] }, "properties": { "name": "460-454 W 16th St", "helps": "W 15th St & 7th Ave" } }'); 14 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0026, 40.7366] }, "properties": { "name": "227 W 11th St", "helps": "W 15th St & 7th Ave" } }'); 15 | 16 | -- E 30th St & Madison Ave (Helps Antenna ID: 2) 17 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9821, 40.7488] }, "properties": { "name": "201-217 Madison Ave", "helps": "E 30th St & Madison Ave" } }'); 18 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9793, 40.7434] }, "properties": { "name": "443 3rd Ave", "helps": "E 30th St & Madison Ave" } }'); 19 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9869, 40.7425] }, "properties": { "name": "36 Madison Ave", "helps": "E 30th St & Madison Ave" } }'); 20 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9896, 40.7473] }, "properties": { "name": "856 6th Ave", "helps": "E 30th St & Madison Ave" } }'); 21 | 22 | -- W 37th St & 9th Ave (Helps Antenna ID: 3) 23 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9926, 40.7586] }, "properties": { "name": "W 42nd St & 9th Ave", "helps": "W 37th St & 9th Ave" } }'); 24 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0004, 40.7580] }, "properties": { "name": "473-455 11th Ave", "helps": "W 37th St & 9th Ave" } }'); 25 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9886, 40.7535] }, "properties": { "name": "7th Avenue and & W 38th St", "helps": "W 37th St & 9th Ave" } }'); 26 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9975, 40.7518] }, "properties": { "name": "370-380 9th Ave", "helps": "W 37th St & 9th Ave" } }'); 27 | 28 | -- W Houston St & Wooster St (Helps Antenna ID: 4) 29 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9955, 40.7295] }, "properties": { "name": "226-242 Greene St", "helps": "W Houston St & Wooster St" } }'); 30 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0028, 40.7285] }, "properties": { "name": "Greenwich Village", "helps": "W Houston St & Wooster St" } }'); 31 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0019, 40.7230] }, "properties": { "name": "Broome St", "helps": "W Houston St & Wooster St" } }'); 32 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9928, 40.7241] }, "properties": { "name": "E Houston St & Bowery", "helps": "W Houston St & Wooster St" } }'); 33 | 34 | -- 1st Ave & E 17th St (Helps Antenna ID: 5) 35 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9791, 40.7360] }, "properties": { "name": "375-361 1st Ave", "helps": "Ave & E 17th St" } }'); 36 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9743, 40.7303] }, "properties": { "name": "E 16th St", "helps": "Ave & E 17th St" } }'); 37 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9841, 40.7293] }, "properties": { "name": "177-171 1st Ave", "helps": "Ave & E 17th St" } }'); 38 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9879, 40.7353] }, "properties": { "name": "E 16th St & Irving Pl", "helps": "Ave & E 17th St" } }'); 39 | 40 | -- Williamsburg Bridge & Pitt St (Helps Antenna ID: 6) 41 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9768, 40.714] }, "properties": { "name": "FDR dr & Williamsburg Bridge", "helps": "Williamsburg Bridge & Pitt St" } }'); 42 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9810, 40.7211] }, "properties": { "name": "12-34 Avenue C", "helps": "Williamsburg Bridge & Pitt St" } }'); 43 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9873, 40.7184] }, "properties": { "name": "85 Delancey St", "helps": "Williamsburg Bridge & Pitt St" } }'); 44 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-73.9851, 40.7130] }, "properties": { "name": "300 Madison St", "helps": "Williamsburg Bridge & Pitt St" } }'); 45 | 46 | -- Broadway & Park Pl (Helps Antenna ID: 7) 47 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0054, 40.715] }, "properties": { "name": "290 Broadway", "helps": "Broadway & Park Pl" } }'); 48 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0013, 40.7101] }, "properties": { "name": "355-365 Pearl St", "helps": "Broadway & Park Pl" } }'); 49 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0100, 40.7097] }, "properties": { "name": "140-172 Broadway", "helps": "Broadway & Park Pl" } }'); 50 | INSERT INTO antennas (geojson) VALUES ('{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-74.0121, 40.7148] }, "properties": { "name": "240 Greenwich St 16th Floor", "helps": "Broadway & Park Pl" } }'); --------------------------------------------------------------------------------