├── .gitignore ├── .nvmrc ├── Dockerfile ├── README.md ├── dataset_templates ├── Chinook.sqlite ├── Empty.sqlite └── TestingEdgeCases.sql ├── package-lock.json ├── package.json ├── src ├── capabilities.ts ├── config.ts ├── datasets.ts ├── db.ts ├── environment.ts ├── index.ts ├── mutation.ts ├── query.ts ├── raw.ts ├── schema.ts └── util.ts ├── test ├── TESTING.md ├── chinook.db └── docker-compose.yml └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | ./*.sqlite 4 | dataset_clones/ 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.17.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app 4 | COPY package.json . 5 | COPY package-lock.json . 6 | 7 | RUN npm ci 8 | 9 | COPY tsconfig.json . 10 | COPY src src 11 | 12 | # This is just to ensure everything compiles ahead of time. 13 | # We'll actually run using ts-node to ensure we get TypesScript 14 | # stack traces if something fails at runtime. 15 | RUN npm run typecheck 16 | 17 | EXPOSE 8100 18 | 19 | # We don't bother doing typechecking when we run (only TS->JS transpiling) 20 | # because we checked it above already. This uses less memory at runtime. 21 | CMD [ "npm", "run", "--silent", "start-no-typecheck" ] 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Connector Agent for SQLite 2 | 3 | This directory contains an SQLite implementation of a data connector agent. 4 | It can use local SQLite database files as referenced by the "db" config field. 5 | 6 | ## Capabilities 7 | 8 | The SQLite agent currently supports the following capabilities: 9 | 10 | * [x] GraphQL Schema 11 | * [x] GraphQL Queries 12 | * [x] Relationships 13 | * [x] Aggregations 14 | * [x] Prometheus Metrics 15 | * [x] Exposing Foreign-Key Information 16 | * [x] Mutations 17 | * [x] Native (Interpolated) Queries 18 | * [ ] Subscriptions 19 | * [ ] Streaming Subscriptions 20 | 21 | Note: You are able to get detailed metadata about the agent's capabilities by 22 | `GET`ting the `/capabilities` endpoint of the running agent. 23 | 24 | ## Requirements 25 | 26 | * NodeJS 16 27 | * SQLite `>= 3.38.0` or compiled in JSON support 28 | * Required for the json_group_array() and json_group_object() aggregate SQL functions 29 | * https://www.sqlite.org/json1.html#jgrouparray 30 | * Note: NPM is used for the [TS Types for the DC-API protocol](https://www.npmjs.com/package/@hasura/dc-api-types) 31 | 32 | ## Build & Run 33 | 34 | ```sh 35 | npm install 36 | npm run build 37 | npm run start 38 | ``` 39 | 40 | Or a simple dev-loop via `entr`: 41 | 42 | ```sh 43 | echo src/**/*.ts | xargs -n1 echo | DB_READONLY=y entr -r npm run start 44 | ``` 45 | 46 | ## Docker Build & Run 47 | 48 | ``` 49 | > docker build . -t dc-sqlite-agent:latest 50 | > docker run -it --rm -p 8100:8100 dc-sqlite-agent:latest 51 | ``` 52 | 53 | You will want to mount a volume with your database(s) so that they can be referenced in configuration. 54 | 55 | ## Options / Environment Variables 56 | 57 | Note: Boolean flags `{FLAG}` can be provided as `1`, `true`, `t`, `yes`, `y`, or omitted and default to `false`. 58 | 59 | | ENV Variable Name | Format | Default | Info | 60 | | --- | --- | --- | --- | 61 | | `PORT` | `INT` | `8100` | Port for agent to listen on. | 62 | | `PERMISSIVE_CORS` | `{FLAG}` | `false` | Allows all requests - Useful for testing with SwaggerUI. Turn off on production. | 63 | | `DB_CREATE` | `{FLAG}` | `false` | Allows new databases to be created. | 64 | | `DB_READONLY` | `{FLAG}` | `false` | Makes databases readonly. | 65 | | `DB_ALLOW_LIST` | `DB1[,DB2]*` | Any Allowed | Restrict what databases can be connected to. | 66 | | `DB_PRIVATECACHE` | `{FLAG}` | Shared | Keep caches between connections private. | 67 | | `DEBUGGING_TAGS` | `{FLAG}` | `false` | Outputs xml style tags in query comments for deugging purposes. | 68 | | `PRETTY_PRINT_LOGS` | `{FLAG}` | `false` | Uses `pino-pretty` to pretty print request logs | 69 | | `LOG_LEVEL` | `fatal` \| `error` \| `info` \| `debug` \| `trace` \| `silent` | `info` | The minimum log level to output | 70 | | `METRICS` | `{FLAG}` | `false` | Enables a `/metrics` prometheus metrics endpoint. 71 | | `QUERY_LENGTH_LIMIT` | `INT` | `Infinity` | Puts a limit on the length of generated SQL before execution. | 72 | | `DATASETS` | `{FLAG}` | `false` | Enable dataset operations | 73 | | `DATASET_DELETE` | `{FLAG}` | `false` | Enable `DELETE /datasets/:name` | 74 | | `DATASET_TEMPLATES` | `DIRECTORY` | `./dataset_templates` | Directory to clone datasets from. | 75 | | `DATASET_CLONES` | `DIRECTORY` | `./dataset_clones` | Directory to clone datasets to. | 76 | | `MUTATIONS` | `{FLAG}` | `false` | Enable Mutation Support. | 77 | 78 | 79 | ## Agent usage 80 | 81 | The agent is configured as per the configuration schema. The valid configuration properties are: 82 | 83 | | Property | Type | Default | 84 | | -------- | ---- | ------- | 85 | | `db` | `string` | | 86 | | `tables` | `string[]` | `null` | 87 | | `include_sqlite_meta_tables` | `boolean` | `false` | 88 | | `explicit_main_schema` | `boolean` | `false ` 89 | 90 | The only required property is `db` which specifies a local sqlite database to use. 91 | 92 | The schema is exposed via introspection, but you can limit which tables are referenced by 93 | 94 | * Explicitly enumerating them via the `tables` property, or 95 | * Toggling the `include_sqlite_meta_tables` to include or exclude sqlite meta tables. 96 | 97 | The `explicit_main_schema` field can be set to opt into exposing tables by their fully qualified names (ie `["main", "MyTable"]` instead of just `["MyTable"]`). 98 | 99 | ## Dataset 100 | 101 | The dataset used for testing the reference agent is sourced from: 102 | 103 | * https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql 104 | 105 | ### Datasets 106 | 107 | Datasets support is enabled via the ENV variables: 108 | 109 | * `DATASETS` 110 | * `DATASET_DELETE` 111 | * `DATASET_TEMPLATES` 112 | * `DATASET_CLONES` 113 | 114 | Templates will be looked up at `${DATASET_TEMPLATES}/${template_name}.sqlite` or `${DATASET_TEMPLATES}/${template_name}.sql`. The `.sqlite` templates are just SQLite database files that will be copied as a clone. The `.sql` templates are SQL script files that will be run against a blank SQLite database in order to create a clone. 115 | 116 | Clones will be copied to `${DATASET_CLONES}/${clone_name}.sqlite`. 117 | 118 | ## Testing Changes to the Agent 119 | 120 | Ensure you run the agent with `DATASETS=1 DATASET_DELETE=1 MUTATIONS=1` in order to enable testing of mutations. 121 | 122 | Then run: 123 | 124 | ```sh 125 | cabal run dc-api:test:tests-dc-api -- test --agent-base-url http://localhost:8100 sandwich --tui 126 | ``` 127 | 128 | From the HGE repo. 129 | 130 | ## Known Issues 131 | * Using "returning" in insert/update/delete mutations where you join across relationships that are affected by the insert/update/delete mutation itself may return inconsistent results. This is because of this issue with SQLite: https://sqlite.org/forum/forumpost/9470611066 132 | 133 | ## TODO 134 | 135 | * [x] Prometheus metrics hosted at `/metrics` 136 | * [x] Pull reference types from a package rather than checked-in files 137 | * [x] Health Check 138 | * [x] DB Specific Health Checks 139 | * [x] Schema 140 | * [x] Capabilities 141 | * [x] Query 142 | * [x] Array Relationships 143 | * [x] Object Relationships 144 | * [x] Ensure everything is escaped correctly - https://sequelize.org/api/v6/class/src/sequelize.js~sequelize#instance-method-escape 145 | * [ ] Or... Use parameterized queries if possible - https://sequelize.org/docs/v6/core-concepts/raw-queries/#bind-parameter 146 | * [x] Run test-suite from SDK 147 | * [x] Remove old queries module 148 | * [x] Relationships / Joins 149 | * [x] Rename `resultTT` and other badly named types in the `schema.ts` module 150 | * [x] Add ENV Variable for restriction on what databases can be used 151 | * [x] Update to the latest types 152 | * [x] Port back to hge codebase as an official reference agent 153 | * [x] Make escapeSQL global to the query module 154 | * [x] Make CORS permissions configurable 155 | * [x] Optional DB Allowlist 156 | * [x] Fix SDK Test suite to be more flexible about descriptions 157 | * [x] READONLY option 158 | * [x] CREATE option 159 | * [x] Don't create DB option 160 | * [x] Aggregate queries 161 | * [x] Verbosity settings 162 | * [x] Cache settings 163 | * [x] Missing WHERE clause from object relationships 164 | * [x] Reuse `find_table_relationship` in more scenarios 165 | * [x] ORDER clause in aggregates breaks SQLite parser for some reason 166 | * [x] Check that looped exist check doesn't cause name conflicts 167 | * [x] `NOT EXISTS IS NULL` != `EXISTS IS NOT NULL` 168 | * [x] Mutation support 169 | -------------------------------------------------------------------------------- /dataset_templates/Chinook.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/sqlite-dataconnector-agent/ffdd864b4ac41d0b05cbf98576e82a7e17decba4/dataset_templates/Chinook.sqlite -------------------------------------------------------------------------------- /dataset_templates/Empty.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/sqlite-dataconnector-agent/ffdd864b4ac41d0b05cbf98576e82a7e17decba4/dataset_templates/Empty.sqlite -------------------------------------------------------------------------------- /dataset_templates/TestingEdgeCases.sql: -------------------------------------------------------------------------------- 1 | -- A table without a primary key 2 | CREATE TABLE NoPrimaryKey ( 3 | [FirstName] VARCHAR(40) NOT NULL, 4 | [LastName] VARCHAR(40) NOT NULL 5 | ); 6 | 7 | INSERT INTO NoPrimaryKey ([FirstName], [LastName]) VALUES ('Jean-Luc', 'Picard'); 8 | INSERT INTO NoPrimaryKey ([FirstName], [LastName]) VALUES ('Will', 'Riker'); 9 | INSERT INTO NoPrimaryKey ([FirstName], [LastName]) VALUES ('Geordi', 'La Forge'); 10 | INSERT INTO NoPrimaryKey ([FirstName], [LastName]) VALUES ('Deanna', 'Troi'); 11 | INSERT INTO NoPrimaryKey ([FirstName], [LastName]) VALUES ('Beverly', 'Crusher'); 12 | 13 | 14 | -- A table with a PK that can be fulfilled by a default value 15 | CREATE TABLE DefaultedPrimaryKey ( 16 | [TimestampKey] DATETIME PRIMARY KEY DEFAULT CURRENT_TIMESTAMP, 17 | [Message] VARCHAR(255) 18 | ); 19 | 20 | INSERT INTO DefaultedPrimaryKey ([TimestampKey], [Message]) VALUES ('2023-02-13 12:23:00', 'Message 1'); 21 | INSERT INTO DefaultedPrimaryKey ([TimestampKey], [Message]) VALUES ('2023-02-13 13:12:01', 'Message 2'); 22 | INSERT INTO DefaultedPrimaryKey ([TimestampKey], [Message]) VALUES ('2023-02-13 16:54:02', 'Message 3'); 23 | INSERT INTO DefaultedPrimaryKey ([TimestampKey], [Message]) VALUES ('2023-02-13 17:31:03', 'Message 4'); 24 | 25 | -- A table where all columns can be generated by the database 26 | -- via defaults, autoincrementing, or nullability 27 | CREATE TABLE AllColumnsDefaultable ( 28 | [Id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 29 | [Message] VARCHAR(255), 30 | [Importance] INT DEFAULT 100 31 | ); 32 | 33 | INSERT INTO AllColumnsDefaultable ([Message]) VALUES ('Message 1'); 34 | INSERT INTO AllColumnsDefaultable ([Message], [Importance]) VALUES ('Message 2', 200); 35 | INSERT INTO AllColumnsDefaultable ([Message], [Importance]) VALUES ('Message 3', 50); 36 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hasura/dc-agent-sqlite", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@hasura/dc-agent-sqlite", 9 | "version": "0.1.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@fastify/cors": "^8.1.0", 13 | "@hasura/dc-api-types": "0.45.0", 14 | "fastify": "^4.13.0", 15 | "fastify-metrics": "^9.2.1", 16 | "nanoid": "^3.3.4", 17 | "openapi3-ts": "^2.0.2", 18 | "pino-pretty": "^8.1.0", 19 | "sqlite-parser": "^1.0.1", 20 | "sqlite3": "^5.1.4", 21 | "sqlstring-sqlite": "^0.1.1" 22 | }, 23 | "devDependencies": { 24 | "@tsconfig/node16": "^1.0.3", 25 | "@types/node": "^16.11.49", 26 | "@types/sqlite3": "^3.1.8", 27 | "@types/xml2js": "^0.4.11", 28 | "ts-node": "^10.9.1", 29 | "typescript": "^4.7.4" 30 | } 31 | }, 32 | "node_modules/@fastify/cors": { 33 | "version": "8.1.0", 34 | "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.1.0.tgz", 35 | "integrity": "sha512-1OmjwyxQZ8GePxa5t1Rpsn2qS56+1ouKMvZufpgJWhXtoCeM/ffA+PsNW8pyslPr4W0E27gVoFqtvHwhXW1U2w==", 36 | "dependencies": { 37 | "fastify-plugin": "^4.0.0", 38 | "mnemonist": "0.39.2" 39 | } 40 | }, 41 | "node_modules/fastify-plugin": { 42 | "version": "4.5.0", 43 | "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.0.tgz", 44 | "integrity": "sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==" 45 | }, 46 | "node_modules/mnemonist": { 47 | "version": "0.39.2", 48 | "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.2.tgz", 49 | "integrity": "sha512-n3ZCEosuMH03DVivZ9N0fcXPWiZrBLEdfSlEJ+S/mJxmk3zuo1ur0dj9URDczFyP1VS3wfiyKzqLLDXoPJ6rPA==", 50 | "dependencies": { 51 | "obliterator": "^2.0.1" 52 | } 53 | }, 54 | "node_modules/obliterator": { 55 | "version": "2.0.4", 56 | "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", 57 | "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" 58 | }, 59 | "node_modules/@hasura/dc-api-types": { 60 | "version": "0.45.0", 61 | "license": "Apache-2.0", 62 | "devDependencies": { 63 | "@tsconfig/node16": "^1.0.3", 64 | "@types/node": "^16.11.49", 65 | "typescript": "^4.7.4" 66 | } 67 | }, 68 | "node_modules/@tsconfig/node16": { 69 | "version": "1.0.3", 70 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", 71 | "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", 72 | "dev": true 73 | }, 74 | "node_modules/@types/node": { 75 | "version": "16.11.49", 76 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.49.tgz", 77 | "integrity": "sha512-Abq9fBviLV93OiXMu+f6r0elxCzRwc0RC5f99cU892uBITL44pTvgvEqlRlPRi8EGcO1z7Cp8A4d0s/p3J/+Nw==", 78 | "dev": true 79 | }, 80 | "node_modules/typescript": { 81 | "version": "4.7.4", 82 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", 83 | "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", 84 | "dev": true, 85 | "bin": { 86 | "tsc": "bin/tsc", 87 | "tsserver": "bin/tsserver" 88 | }, 89 | "engines": { 90 | "node": ">=4.2.0" 91 | } 92 | }, 93 | "node_modules/fastify": { 94 | "version": "4.13.0", 95 | "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.13.0.tgz", 96 | "integrity": "sha512-p9ibdFWH3pZ7KPgmfHPKGUy2W4EWU2TEpwlcu58w4CwGyU3ARFfh2kwq6zpZ5W2ZGVbufi4tZbqHIHAlX/9Z/A==", 97 | "dependencies": { 98 | "@fastify/ajv-compiler": "^3.3.1", 99 | "@fastify/error": "^3.0.0", 100 | "@fastify/fast-json-stringify-compiler": "^4.1.0", 101 | "abstract-logging": "^2.0.1", 102 | "avvio": "^8.2.0", 103 | "fast-content-type-parse": "^1.0.0", 104 | "find-my-way": "^7.3.0", 105 | "light-my-request": "^5.6.1", 106 | "pino": "^8.5.0", 107 | "process-warning": "^2.0.0", 108 | "proxy-addr": "^2.0.7", 109 | "rfdc": "^1.3.0", 110 | "secure-json-parse": "^2.5.0", 111 | "semver": "^7.3.7", 112 | "tiny-lru": "^10.0.0" 113 | } 114 | }, 115 | "node_modules/@fastify/ajv-compiler": { 116 | "version": "3.4.0", 117 | "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.4.0.tgz", 118 | "integrity": "sha512-69JnK7Cot+ktn7LD5TikP3b7psBPX55tYpQa8WSumt8r117PCa2zwHnImfBtRWYExreJlI48hr0WZaVrTBGj7w==", 119 | "dependencies": { 120 | "ajv": "^8.11.0", 121 | "ajv-formats": "^2.1.1", 122 | "fast-uri": "^2.0.0" 123 | } 124 | }, 125 | "node_modules/ajv": { 126 | "version": "8.11.2", 127 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", 128 | "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", 129 | "dependencies": { 130 | "fast-deep-equal": "^3.1.1", 131 | "json-schema-traverse": "^1.0.0", 132 | "require-from-string": "^2.0.2", 133 | "uri-js": "^4.2.2" 134 | }, 135 | "funding": { 136 | "type": "github", 137 | "url": "https://github.com/sponsors/epoberezkin" 138 | } 139 | }, 140 | "node_modules/fast-deep-equal": { 141 | "version": "3.1.3", 142 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 143 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 144 | }, 145 | "node_modules/json-schema-traverse": { 146 | "version": "1.0.0", 147 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 148 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" 149 | }, 150 | "node_modules/require-from-string": { 151 | "version": "2.0.2", 152 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 153 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 154 | "engines": { 155 | "node": ">=0.10.0" 156 | } 157 | }, 158 | "node_modules/uri-js": { 159 | "version": "4.4.1", 160 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 161 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 162 | "dependencies": { 163 | "punycode": "^2.1.0" 164 | } 165 | }, 166 | "node_modules/punycode": { 167 | "version": "2.1.1", 168 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 169 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 170 | "engines": { 171 | "node": ">=6" 172 | } 173 | }, 174 | "node_modules/ajv-formats": { 175 | "version": "2.1.1", 176 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", 177 | "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", 178 | "dependencies": { 179 | "ajv": "^8.0.0" 180 | }, 181 | "peerDependencies": { 182 | "ajv": "^8.0.0" 183 | }, 184 | "peerDependenciesMeta": { 185 | "ajv": { 186 | "optional": true 187 | } 188 | } 189 | }, 190 | "node_modules/fast-uri": { 191 | "version": "2.1.0", 192 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.1.0.tgz", 193 | "integrity": "sha512-qKRta6N7BWEFVlyonVY/V+BMLgFqktCUV0QjT259ekAIlbVrMaFnFLxJ4s/JPl4tou56S1BzPufI60bLe29fHA==" 194 | }, 195 | "node_modules/@fastify/error": { 196 | "version": "3.0.0", 197 | "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.0.0.tgz", 198 | "integrity": "sha512-dPRyT40GiHRzSCll3/Jn2nPe25+E1VXc9tDwRAIKwFCxd5Np5wzgz1tmooWG3sV0qKgrBibihVoCna2ru4SEFg==" 199 | }, 200 | "node_modules/@fastify/fast-json-stringify-compiler": { 201 | "version": "4.1.0", 202 | "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.1.0.tgz", 203 | "integrity": "sha512-cTKBV2J9+u6VaKDhX7HepSfPSzw+F+TSd+k0wzifj4rG+4E5PjSFJCk19P8R6tr/72cuzgGd+mbB3jFT6lvAgw==", 204 | "dependencies": { 205 | "fast-json-stringify": "^5.0.0" 206 | } 207 | }, 208 | "node_modules/fast-json-stringify": { 209 | "version": "5.4.1", 210 | "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.4.1.tgz", 211 | "integrity": "sha512-P7S9WXEnMqu6seBnzAFmgZ+T3KCD+Do+pNIJsmk/6OlDHZVjl6KzsQB3TFHKQb2Q8N7C9l31WS7/LZGF5hT1FA==", 212 | "dependencies": { 213 | "@fastify/deepmerge": "^1.0.0", 214 | "ajv": "^8.10.0", 215 | "ajv-formats": "^2.1.1", 216 | "fast-deep-equal": "^3.1.3", 217 | "fast-uri": "^2.1.0", 218 | "rfdc": "^1.2.0" 219 | } 220 | }, 221 | "node_modules/@fastify/deepmerge": { 222 | "version": "1.1.0", 223 | "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.1.0.tgz", 224 | "integrity": "sha512-E8Hfdvs1bG6u0N4vN5Nty6JONUfTdOciyD5rn8KnEsLKIenvOVcr210BQR9t34PRkNyjqnMLGk3e0BsaxRdL+g==" 225 | }, 226 | "node_modules/rfdc": { 227 | "version": "1.3.0", 228 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", 229 | "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" 230 | }, 231 | "node_modules/abstract-logging": { 232 | "version": "2.0.1", 233 | "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", 234 | "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" 235 | }, 236 | "node_modules/avvio": { 237 | "version": "8.2.0", 238 | "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.0.tgz", 239 | "integrity": "sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg==", 240 | "dependencies": { 241 | "archy": "^1.0.0", 242 | "debug": "^4.0.0", 243 | "fastq": "^1.6.1" 244 | } 245 | }, 246 | "node_modules/archy": { 247 | "version": "1.0.0", 248 | "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", 249 | "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" 250 | }, 251 | "node_modules/debug": { 252 | "version": "4.3.4", 253 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 254 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 255 | "dependencies": { 256 | "ms": "2.1.2" 257 | }, 258 | "engines": { 259 | "node": ">=6.0" 260 | }, 261 | "peerDependenciesMeta": { 262 | "supports-color": { 263 | "optional": true 264 | } 265 | } 266 | }, 267 | "node_modules/ms": { 268 | "version": "2.1.2", 269 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 270 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 271 | }, 272 | "node_modules/fastq": { 273 | "version": "1.13.0", 274 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 275 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 276 | "dependencies": { 277 | "reusify": "^1.0.4" 278 | } 279 | }, 280 | "node_modules/reusify": { 281 | "version": "1.0.4", 282 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 283 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 284 | "engines": { 285 | "iojs": ">=1.0.0", 286 | "node": ">=0.10.0" 287 | } 288 | }, 289 | "node_modules/fast-content-type-parse": { 290 | "version": "1.0.0", 291 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.0.0.tgz", 292 | "integrity": "sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==" 293 | }, 294 | "node_modules/find-my-way": { 295 | "version": "7.3.1", 296 | "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.3.1.tgz", 297 | "integrity": "sha512-kGvM08SOkqvheLcuQ8GW9t/H901Qb9rZEbcNWbXopzy4jDRoaJpJoObPSKf4MnQLZ20ZTp7rL5MpF6rf+pqmyg==", 298 | "dependencies": { 299 | "fast-deep-equal": "^3.1.3", 300 | "fast-querystring": "^1.0.0", 301 | "safe-regex2": "^2.0.0" 302 | }, 303 | "engines": { 304 | "node": ">=14" 305 | } 306 | }, 307 | "node_modules/fast-querystring": { 308 | "version": "1.0.0", 309 | "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.0.0.tgz", 310 | "integrity": "sha512-3LQi62IhQoDlmt4ULCYmh17vRO2EtS7hTSsG4WwoKWgV7GLMKBOecEh+aiavASnLx8I2y89OD33AGLo0ccRhzA==", 311 | "dependencies": { 312 | "fast-decode-uri-component": "^1.0.1" 313 | } 314 | }, 315 | "node_modules/fast-decode-uri-component": { 316 | "version": "1.0.1", 317 | "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", 318 | "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" 319 | }, 320 | "node_modules/safe-regex2": { 321 | "version": "2.0.0", 322 | "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", 323 | "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", 324 | "dependencies": { 325 | "ret": "~0.2.0" 326 | } 327 | }, 328 | "node_modules/ret": { 329 | "version": "0.2.2", 330 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", 331 | "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", 332 | "engines": { 333 | "node": ">=4" 334 | } 335 | }, 336 | "node_modules/light-my-request": { 337 | "version": "5.6.1", 338 | "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.6.1.tgz", 339 | "integrity": "sha512-sbJnC1UBRivi9L1kICr3CESb82pNiPNB3TvtdIrZZqW0Qh8uDXvoywMmWKZlihDcmw952CMICCzM+54LDf+E+g==", 340 | "dependencies": { 341 | "cookie": "^0.5.0", 342 | "process-warning": "^2.0.0", 343 | "set-cookie-parser": "^2.4.1" 344 | } 345 | }, 346 | "node_modules/cookie": { 347 | "version": "0.5.0", 348 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 349 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 350 | "engines": { 351 | "node": ">= 0.6" 352 | } 353 | }, 354 | "node_modules/process-warning": { 355 | "version": "2.0.0", 356 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", 357 | "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==" 358 | }, 359 | "node_modules/set-cookie-parser": { 360 | "version": "2.5.1", 361 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz", 362 | "integrity": "sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==" 363 | }, 364 | "node_modules/pino": { 365 | "version": "8.7.0", 366 | "resolved": "https://registry.npmjs.org/pino/-/pino-8.7.0.tgz", 367 | "integrity": "sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA==", 368 | "dependencies": { 369 | "atomic-sleep": "^1.0.0", 370 | "fast-redact": "^3.1.1", 371 | "on-exit-leak-free": "^2.1.0", 372 | "pino-abstract-transport": "v1.0.0", 373 | "pino-std-serializers": "^6.0.0", 374 | "process-warning": "^2.0.0", 375 | "quick-format-unescaped": "^4.0.3", 376 | "real-require": "^0.2.0", 377 | "safe-stable-stringify": "^2.3.1", 378 | "sonic-boom": "^3.1.0", 379 | "thread-stream": "^2.0.0" 380 | }, 381 | "bin": { 382 | "pino": "bin.js" 383 | } 384 | }, 385 | "node_modules/atomic-sleep": { 386 | "version": "1.0.0", 387 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 388 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 389 | "engines": { 390 | "node": ">=8.0.0" 391 | } 392 | }, 393 | "node_modules/fast-redact": { 394 | "version": "3.1.2", 395 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", 396 | "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==", 397 | "engines": { 398 | "node": ">=6" 399 | } 400 | }, 401 | "node_modules/on-exit-leak-free": { 402 | "version": "2.1.0", 403 | "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", 404 | "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" 405 | }, 406 | "node_modules/pino-abstract-transport": { 407 | "version": "1.0.0", 408 | "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", 409 | "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", 410 | "dependencies": { 411 | "readable-stream": "^4.0.0", 412 | "split2": "^4.0.0" 413 | } 414 | }, 415 | "node_modules/readable-stream": { 416 | "version": "4.2.0", 417 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz", 418 | "integrity": "sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==", 419 | "dependencies": { 420 | "abort-controller": "^3.0.0", 421 | "buffer": "^6.0.3", 422 | "events": "^3.3.0", 423 | "process": "^0.11.10" 424 | }, 425 | "engines": { 426 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 427 | } 428 | }, 429 | "node_modules/abort-controller": { 430 | "version": "3.0.0", 431 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 432 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 433 | "dependencies": { 434 | "event-target-shim": "^5.0.0" 435 | }, 436 | "engines": { 437 | "node": ">=6.5" 438 | } 439 | }, 440 | "node_modules/event-target-shim": { 441 | "version": "5.0.1", 442 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 443 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 444 | "engines": { 445 | "node": ">=6" 446 | } 447 | }, 448 | "node_modules/buffer": { 449 | "version": "6.0.3", 450 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 451 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 452 | "funding": [ 453 | { 454 | "type": "github", 455 | "url": "https://github.com/sponsors/feross" 456 | }, 457 | { 458 | "type": "patreon", 459 | "url": "https://www.patreon.com/feross" 460 | }, 461 | { 462 | "type": "consulting", 463 | "url": "https://feross.org/support" 464 | } 465 | ], 466 | "dependencies": { 467 | "base64-js": "^1.3.1", 468 | "ieee754": "^1.2.1" 469 | } 470 | }, 471 | "node_modules/base64-js": { 472 | "version": "1.5.1", 473 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 474 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 475 | "funding": [ 476 | { 477 | "type": "github", 478 | "url": "https://github.com/sponsors/feross" 479 | }, 480 | { 481 | "type": "patreon", 482 | "url": "https://www.patreon.com/feross" 483 | }, 484 | { 485 | "type": "consulting", 486 | "url": "https://feross.org/support" 487 | } 488 | ] 489 | }, 490 | "node_modules/ieee754": { 491 | "version": "1.2.1", 492 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 493 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 494 | "funding": [ 495 | { 496 | "type": "github", 497 | "url": "https://github.com/sponsors/feross" 498 | }, 499 | { 500 | "type": "patreon", 501 | "url": "https://www.patreon.com/feross" 502 | }, 503 | { 504 | "type": "consulting", 505 | "url": "https://feross.org/support" 506 | } 507 | ] 508 | }, 509 | "node_modules/events": { 510 | "version": "3.3.0", 511 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 512 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 513 | "engines": { 514 | "node": ">=0.8.x" 515 | } 516 | }, 517 | "node_modules/process": { 518 | "version": "0.11.10", 519 | "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 520 | "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 521 | "engines": { 522 | "node": ">= 0.6.0" 523 | } 524 | }, 525 | "node_modules/split2": { 526 | "version": "4.1.0", 527 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", 528 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", 529 | "engines": { 530 | "node": ">= 10.x" 531 | } 532 | }, 533 | "node_modules/pino-std-serializers": { 534 | "version": "6.0.0", 535 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz", 536 | "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==" 537 | }, 538 | "node_modules/quick-format-unescaped": { 539 | "version": "4.0.4", 540 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 541 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 542 | }, 543 | "node_modules/real-require": { 544 | "version": "0.2.0", 545 | "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 546 | "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 547 | "engines": { 548 | "node": ">= 12.13.0" 549 | } 550 | }, 551 | "node_modules/safe-stable-stringify": { 552 | "version": "2.4.1", 553 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", 554 | "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==", 555 | "engines": { 556 | "node": ">=10" 557 | } 558 | }, 559 | "node_modules/sonic-boom": { 560 | "version": "3.2.0", 561 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz", 562 | "integrity": "sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==", 563 | "dependencies": { 564 | "atomic-sleep": "^1.0.0" 565 | } 566 | }, 567 | "node_modules/thread-stream": { 568 | "version": "2.2.0", 569 | "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz", 570 | "integrity": "sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==", 571 | "dependencies": { 572 | "real-require": "^0.2.0" 573 | } 574 | }, 575 | "node_modules/proxy-addr": { 576 | "version": "2.0.7", 577 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 578 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 579 | "dependencies": { 580 | "forwarded": "0.2.0", 581 | "ipaddr.js": "1.9.1" 582 | }, 583 | "engines": { 584 | "node": ">= 0.10" 585 | } 586 | }, 587 | "node_modules/forwarded": { 588 | "version": "0.2.0", 589 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 590 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 591 | "engines": { 592 | "node": ">= 0.6" 593 | } 594 | }, 595 | "node_modules/ipaddr.js": { 596 | "version": "1.9.1", 597 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 598 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 599 | "engines": { 600 | "node": ">= 0.10" 601 | } 602 | }, 603 | "node_modules/secure-json-parse": { 604 | "version": "2.5.0", 605 | "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.5.0.tgz", 606 | "integrity": "sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==" 607 | }, 608 | "node_modules/semver": { 609 | "version": "7.3.8", 610 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", 611 | "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", 612 | "dependencies": { 613 | "lru-cache": "^6.0.0" 614 | }, 615 | "bin": { 616 | "semver": "bin/semver.js" 617 | }, 618 | "engines": { 619 | "node": ">=10" 620 | } 621 | }, 622 | "node_modules/lru-cache": { 623 | "version": "6.0.0", 624 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 625 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 626 | "dependencies": { 627 | "yallist": "^4.0.0" 628 | }, 629 | "engines": { 630 | "node": ">=10" 631 | } 632 | }, 633 | "node_modules/yallist": { 634 | "version": "4.0.0", 635 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 636 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 637 | }, 638 | "node_modules/tiny-lru": { 639 | "version": "10.0.1", 640 | "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.0.1.tgz", 641 | "integrity": "sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==", 642 | "engines": { 643 | "node": ">=6" 644 | } 645 | }, 646 | "node_modules/fastify-metrics": { 647 | "version": "9.2.1", 648 | "license": "MIT", 649 | "dependencies": { 650 | "fastify-plugin": "^4.0.0", 651 | "prom-client": "^14.0.1" 652 | }, 653 | "peerDependencies": { 654 | "fastify": "^4.0.0" 655 | } 656 | }, 657 | "node_modules/prom-client": { 658 | "version": "14.0.1", 659 | "license": "Apache-2.0", 660 | "dependencies": { 661 | "tdigest": "^0.1.1" 662 | }, 663 | "engines": { 664 | "node": ">=10" 665 | } 666 | }, 667 | "node_modules/tdigest": { 668 | "version": "0.1.2", 669 | "license": "MIT", 670 | "dependencies": { 671 | "bintrees": "1.0.2" 672 | } 673 | }, 674 | "node_modules/bintrees": { 675 | "version": "1.0.2", 676 | "license": "MIT" 677 | }, 678 | "node_modules/nanoid": { 679 | "version": "3.3.4", 680 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 681 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", 682 | "bin": { 683 | "nanoid": "bin/nanoid.cjs" 684 | }, 685 | "engines": { 686 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 687 | } 688 | }, 689 | "node_modules/openapi3-ts": { 690 | "version": "2.0.2", 691 | "license": "MIT", 692 | "dependencies": { 693 | "yaml": "^1.10.2" 694 | } 695 | }, 696 | "node_modules/yaml": { 697 | "version": "1.10.2", 698 | "license": "ISC", 699 | "engines": { 700 | "node": ">= 6" 701 | } 702 | }, 703 | "node_modules/pino-pretty": { 704 | "version": "8.1.0", 705 | "license": "MIT", 706 | "dependencies": { 707 | "colorette": "^2.0.7", 708 | "dateformat": "^4.6.3", 709 | "fast-copy": "^2.1.1", 710 | "fast-safe-stringify": "^2.1.1", 711 | "help-me": "^4.0.1", 712 | "joycon": "^3.1.1", 713 | "minimist": "^1.2.6", 714 | "on-exit-leak-free": "^1.0.0", 715 | "pino-abstract-transport": "^1.0.0", 716 | "pump": "^3.0.0", 717 | "readable-stream": "^4.0.0", 718 | "secure-json-parse": "^2.4.0", 719 | "sonic-boom": "^3.0.0", 720 | "strip-json-comments": "^3.1.1" 721 | }, 722 | "bin": { 723 | "pino-pretty": "bin.js" 724 | } 725 | }, 726 | "node_modules/colorette": { 727 | "version": "2.0.16", 728 | "license": "MIT" 729 | }, 730 | "node_modules/dateformat": { 731 | "version": "4.6.3", 732 | "license": "MIT", 733 | "engines": { 734 | "node": "*" 735 | } 736 | }, 737 | "node_modules/fast-copy": { 738 | "version": "2.1.3", 739 | "license": "MIT" 740 | }, 741 | "node_modules/fast-safe-stringify": { 742 | "version": "2.1.1", 743 | "license": "MIT" 744 | }, 745 | "node_modules/help-me": { 746 | "version": "4.0.1", 747 | "license": "MIT", 748 | "dependencies": { 749 | "glob": "^8.0.0", 750 | "readable-stream": "^3.6.0" 751 | } 752 | }, 753 | "node_modules/help-me/node_modules/glob": { 754 | "version": "8.0.3", 755 | "license": "ISC", 756 | "dependencies": { 757 | "fs.realpath": "^1.0.0", 758 | "inflight": "^1.0.4", 759 | "inherits": "2", 760 | "minimatch": "^5.0.1", 761 | "once": "^1.3.0" 762 | }, 763 | "engines": { 764 | "node": ">=12" 765 | }, 766 | "funding": { 767 | "url": "https://github.com/sponsors/isaacs" 768 | } 769 | }, 770 | "node_modules/fs.realpath": { 771 | "version": "1.0.0", 772 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 773 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 774 | }, 775 | "node_modules/inflight": { 776 | "version": "1.0.6", 777 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 778 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 779 | "dependencies": { 780 | "once": "^1.3.0", 781 | "wrappy": "1" 782 | } 783 | }, 784 | "node_modules/once": { 785 | "version": "1.4.0", 786 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 787 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 788 | "dependencies": { 789 | "wrappy": "1" 790 | } 791 | }, 792 | "node_modules/wrappy": { 793 | "version": "1.0.2", 794 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 795 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 796 | }, 797 | "node_modules/inherits": { 798 | "version": "2.0.4", 799 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 800 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 801 | }, 802 | "node_modules/help-me/node_modules/minimatch": { 803 | "version": "5.1.0", 804 | "license": "ISC", 805 | "dependencies": { 806 | "brace-expansion": "^2.0.1" 807 | }, 808 | "engines": { 809 | "node": ">=10" 810 | } 811 | }, 812 | "node_modules/help-me/node_modules/brace-expansion": { 813 | "version": "2.0.1", 814 | "license": "MIT", 815 | "dependencies": { 816 | "balanced-match": "^1.0.0" 817 | } 818 | }, 819 | "node_modules/balanced-match": { 820 | "version": "1.0.2", 821 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 822 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 823 | }, 824 | "node_modules/help-me/node_modules/readable-stream": { 825 | "version": "3.6.0", 826 | "license": "MIT", 827 | "dependencies": { 828 | "inherits": "^2.0.3", 829 | "string_decoder": "^1.1.1", 830 | "util-deprecate": "^1.0.1" 831 | }, 832 | "engines": { 833 | "node": ">= 6" 834 | } 835 | }, 836 | "node_modules/string_decoder": { 837 | "version": "1.3.0", 838 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 839 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 840 | "dependencies": { 841 | "safe-buffer": "~5.2.0" 842 | } 843 | }, 844 | "node_modules/safe-buffer": { 845 | "version": "5.2.1", 846 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 847 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 848 | "funding": [ 849 | { 850 | "type": "github", 851 | "url": "https://github.com/sponsors/feross" 852 | }, 853 | { 854 | "type": "patreon", 855 | "url": "https://www.patreon.com/feross" 856 | }, 857 | { 858 | "type": "consulting", 859 | "url": "https://feross.org/support" 860 | } 861 | ] 862 | }, 863 | "node_modules/util-deprecate": { 864 | "version": "1.0.2", 865 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 866 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 867 | }, 868 | "node_modules/joycon": { 869 | "version": "3.1.1", 870 | "license": "MIT", 871 | "engines": { 872 | "node": ">=10" 873 | } 874 | }, 875 | "node_modules/minimist": { 876 | "version": "1.2.6", 877 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 878 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" 879 | }, 880 | "node_modules/pino-pretty/node_modules/on-exit-leak-free": { 881 | "version": "1.0.0", 882 | "license": "MIT" 883 | }, 884 | "node_modules/pump": { 885 | "version": "3.0.0", 886 | "license": "MIT", 887 | "dependencies": { 888 | "end-of-stream": "^1.1.0", 889 | "once": "^1.3.1" 890 | } 891 | }, 892 | "node_modules/end-of-stream": { 893 | "version": "1.4.4", 894 | "license": "MIT", 895 | "dependencies": { 896 | "once": "^1.4.0" 897 | } 898 | }, 899 | "node_modules/pino-pretty/node_modules/readable-stream": { 900 | "version": "4.1.0", 901 | "license": "MIT", 902 | "dependencies": { 903 | "abort-controller": "^3.0.0" 904 | }, 905 | "engines": { 906 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 907 | } 908 | }, 909 | "node_modules/strip-json-comments": { 910 | "version": "3.1.1", 911 | "license": "MIT", 912 | "engines": { 913 | "node": ">=8" 914 | }, 915 | "funding": { 916 | "url": "https://github.com/sponsors/sindresorhus" 917 | } 918 | }, 919 | "node_modules/sqlite-parser": { 920 | "version": "1.0.1", 921 | "license": "MIT", 922 | "bin": { 923 | "sqlite-parser": "bin/sqlite-parser" 924 | }, 925 | "engines": { 926 | "node": ">=4" 927 | } 928 | }, 929 | "node_modules/sqlite3": { 930 | "version": "5.1.4", 931 | "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.4.tgz", 932 | "integrity": "sha512-i0UlWAzPlzX3B5XP2cYuhWQJsTtlMD6obOa1PgeEQ4DHEXUuyJkgv50I3isqZAP5oFc2T8OFvakmDh2W6I+YpA==", 933 | "hasInstallScript": true, 934 | "dependencies": { 935 | "@mapbox/node-pre-gyp": "^1.0.0", 936 | "node-addon-api": "^4.2.0", 937 | "tar": "^6.1.11" 938 | }, 939 | "optionalDependencies": { 940 | "node-gyp": "8.x" 941 | }, 942 | "peerDependencies": { 943 | "node-gyp": "8.x" 944 | }, 945 | "peerDependenciesMeta": { 946 | "node-gyp": { 947 | "optional": true 948 | } 949 | } 950 | }, 951 | "node_modules/@mapbox/node-pre-gyp": { 952 | "version": "1.0.10", 953 | "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", 954 | "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", 955 | "dependencies": { 956 | "detect-libc": "^2.0.0", 957 | "https-proxy-agent": "^5.0.0", 958 | "make-dir": "^3.1.0", 959 | "node-fetch": "^2.6.7", 960 | "nopt": "^5.0.0", 961 | "npmlog": "^5.0.1", 962 | "rimraf": "^3.0.2", 963 | "semver": "^7.3.5", 964 | "tar": "^6.1.11" 965 | }, 966 | "bin": { 967 | "node-pre-gyp": "bin/node-pre-gyp" 968 | } 969 | }, 970 | "node_modules/detect-libc": { 971 | "version": "2.0.1", 972 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", 973 | "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", 974 | "engines": { 975 | "node": ">=8" 976 | } 977 | }, 978 | "node_modules/https-proxy-agent": { 979 | "version": "5.0.1", 980 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 981 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 982 | "dependencies": { 983 | "agent-base": "6", 984 | "debug": "4" 985 | }, 986 | "engines": { 987 | "node": ">= 6" 988 | } 989 | }, 990 | "node_modules/agent-base": { 991 | "version": "6.0.2", 992 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 993 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 994 | "dependencies": { 995 | "debug": "4" 996 | }, 997 | "engines": { 998 | "node": ">= 6.0.0" 999 | } 1000 | }, 1001 | "node_modules/make-dir": { 1002 | "version": "3.1.0", 1003 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 1004 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 1005 | "dependencies": { 1006 | "semver": "^6.0.0" 1007 | }, 1008 | "engines": { 1009 | "node": ">=8" 1010 | }, 1011 | "funding": { 1012 | "url": "https://github.com/sponsors/sindresorhus" 1013 | } 1014 | }, 1015 | "node_modules/make-dir/node_modules/semver": { 1016 | "version": "6.3.0", 1017 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1018 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1019 | "bin": { 1020 | "semver": "bin/semver.js" 1021 | } 1022 | }, 1023 | "node_modules/node-fetch": { 1024 | "version": "2.6.9", 1025 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", 1026 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", 1027 | "dependencies": { 1028 | "whatwg-url": "^5.0.0" 1029 | }, 1030 | "engines": { 1031 | "node": "4.x || >=6.0.0" 1032 | }, 1033 | "peerDependencies": { 1034 | "encoding": "^0.1.0" 1035 | }, 1036 | "peerDependenciesMeta": { 1037 | "encoding": { 1038 | "optional": true 1039 | } 1040 | } 1041 | }, 1042 | "node_modules/whatwg-url": { 1043 | "version": "5.0.0", 1044 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1045 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1046 | "dependencies": { 1047 | "tr46": "~0.0.3", 1048 | "webidl-conversions": "^3.0.0" 1049 | } 1050 | }, 1051 | "node_modules/tr46": { 1052 | "version": "0.0.3", 1053 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1054 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1055 | }, 1056 | "node_modules/webidl-conversions": { 1057 | "version": "3.0.1", 1058 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1059 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1060 | }, 1061 | "node_modules/encoding": { 1062 | "version": "0.1.13", 1063 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", 1064 | "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", 1065 | "optional": true, 1066 | "dependencies": { 1067 | "iconv-lite": "^0.6.2" 1068 | } 1069 | }, 1070 | "node_modules/iconv-lite": { 1071 | "version": "0.6.3", 1072 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 1073 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 1074 | "optional": true, 1075 | "dependencies": { 1076 | "safer-buffer": ">= 2.1.2 < 3.0.0" 1077 | }, 1078 | "engines": { 1079 | "node": ">=0.10.0" 1080 | } 1081 | }, 1082 | "node_modules/safer-buffer": { 1083 | "version": "2.1.2", 1084 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1085 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1086 | "optional": true 1087 | }, 1088 | "node_modules/nopt": { 1089 | "version": "5.0.0", 1090 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", 1091 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", 1092 | "dependencies": { 1093 | "abbrev": "1" 1094 | }, 1095 | "bin": { 1096 | "nopt": "bin/nopt.js" 1097 | }, 1098 | "engines": { 1099 | "node": ">=6" 1100 | } 1101 | }, 1102 | "node_modules/abbrev": { 1103 | "version": "1.1.1", 1104 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 1105 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 1106 | }, 1107 | "node_modules/npmlog": { 1108 | "version": "5.0.1", 1109 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", 1110 | "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", 1111 | "dependencies": { 1112 | "are-we-there-yet": "^2.0.0", 1113 | "console-control-strings": "^1.1.0", 1114 | "gauge": "^3.0.0", 1115 | "set-blocking": "^2.0.0" 1116 | } 1117 | }, 1118 | "node_modules/are-we-there-yet": { 1119 | "version": "2.0.0", 1120 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", 1121 | "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", 1122 | "dependencies": { 1123 | "delegates": "^1.0.0", 1124 | "readable-stream": "^3.6.0" 1125 | }, 1126 | "engines": { 1127 | "node": ">=10" 1128 | } 1129 | }, 1130 | "node_modules/delegates": { 1131 | "version": "1.0.0", 1132 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 1133 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" 1134 | }, 1135 | "node_modules/are-we-there-yet/node_modules/readable-stream": { 1136 | "version": "3.6.1", 1137 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", 1138 | "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", 1139 | "dependencies": { 1140 | "inherits": "^2.0.3", 1141 | "string_decoder": "^1.1.1", 1142 | "util-deprecate": "^1.0.1" 1143 | }, 1144 | "engines": { 1145 | "node": ">= 6" 1146 | } 1147 | }, 1148 | "node_modules/console-control-strings": { 1149 | "version": "1.1.0", 1150 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 1151 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" 1152 | }, 1153 | "node_modules/gauge": { 1154 | "version": "3.0.2", 1155 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", 1156 | "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", 1157 | "dependencies": { 1158 | "aproba": "^1.0.3 || ^2.0.0", 1159 | "color-support": "^1.1.2", 1160 | "console-control-strings": "^1.0.0", 1161 | "has-unicode": "^2.0.1", 1162 | "object-assign": "^4.1.1", 1163 | "signal-exit": "^3.0.0", 1164 | "string-width": "^4.2.3", 1165 | "strip-ansi": "^6.0.1", 1166 | "wide-align": "^1.1.2" 1167 | }, 1168 | "engines": { 1169 | "node": ">=10" 1170 | } 1171 | }, 1172 | "node_modules/aproba": { 1173 | "version": "2.0.0", 1174 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", 1175 | "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" 1176 | }, 1177 | "node_modules/color-support": { 1178 | "version": "1.1.3", 1179 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 1180 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", 1181 | "bin": { 1182 | "color-support": "bin.js" 1183 | } 1184 | }, 1185 | "node_modules/has-unicode": { 1186 | "version": "2.0.1", 1187 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 1188 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" 1189 | }, 1190 | "node_modules/object-assign": { 1191 | "version": "4.1.1", 1192 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1193 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1194 | "engines": { 1195 | "node": ">=0.10.0" 1196 | } 1197 | }, 1198 | "node_modules/signal-exit": { 1199 | "version": "3.0.7", 1200 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 1201 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 1202 | }, 1203 | "node_modules/string-width": { 1204 | "version": "4.2.3", 1205 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1206 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1207 | "dependencies": { 1208 | "emoji-regex": "^8.0.0", 1209 | "is-fullwidth-code-point": "^3.0.0", 1210 | "strip-ansi": "^6.0.1" 1211 | }, 1212 | "engines": { 1213 | "node": ">=8" 1214 | } 1215 | }, 1216 | "node_modules/emoji-regex": { 1217 | "version": "8.0.0", 1218 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1219 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1220 | }, 1221 | "node_modules/is-fullwidth-code-point": { 1222 | "version": "3.0.0", 1223 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1224 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1225 | "engines": { 1226 | "node": ">=8" 1227 | } 1228 | }, 1229 | "node_modules/strip-ansi": { 1230 | "version": "6.0.1", 1231 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1232 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1233 | "dependencies": { 1234 | "ansi-regex": "^5.0.1" 1235 | }, 1236 | "engines": { 1237 | "node": ">=8" 1238 | } 1239 | }, 1240 | "node_modules/ansi-regex": { 1241 | "version": "5.0.1", 1242 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1243 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1244 | "engines": { 1245 | "node": ">=8" 1246 | } 1247 | }, 1248 | "node_modules/wide-align": { 1249 | "version": "1.1.5", 1250 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", 1251 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", 1252 | "dependencies": { 1253 | "string-width": "^1.0.2 || 2 || 3 || 4" 1254 | } 1255 | }, 1256 | "node_modules/set-blocking": { 1257 | "version": "2.0.0", 1258 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1259 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" 1260 | }, 1261 | "node_modules/rimraf": { 1262 | "version": "3.0.2", 1263 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1264 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1265 | "dependencies": { 1266 | "glob": "^7.1.3" 1267 | }, 1268 | "bin": { 1269 | "rimraf": "bin.js" 1270 | }, 1271 | "funding": { 1272 | "url": "https://github.com/sponsors/isaacs" 1273 | } 1274 | }, 1275 | "node_modules/glob": { 1276 | "version": "7.2.3", 1277 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 1278 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 1279 | "dependencies": { 1280 | "fs.realpath": "^1.0.0", 1281 | "inflight": "^1.0.4", 1282 | "inherits": "2", 1283 | "minimatch": "^3.1.1", 1284 | "once": "^1.3.0", 1285 | "path-is-absolute": "^1.0.0" 1286 | }, 1287 | "engines": { 1288 | "node": "*" 1289 | }, 1290 | "funding": { 1291 | "url": "https://github.com/sponsors/isaacs" 1292 | } 1293 | }, 1294 | "node_modules/minimatch": { 1295 | "version": "3.1.2", 1296 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1297 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1298 | "dependencies": { 1299 | "brace-expansion": "^1.1.7" 1300 | }, 1301 | "engines": { 1302 | "node": "*" 1303 | } 1304 | }, 1305 | "node_modules/brace-expansion": { 1306 | "version": "1.1.11", 1307 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1308 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1309 | "dependencies": { 1310 | "balanced-match": "^1.0.0", 1311 | "concat-map": "0.0.1" 1312 | } 1313 | }, 1314 | "node_modules/concat-map": { 1315 | "version": "0.0.1", 1316 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1317 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 1318 | }, 1319 | "node_modules/path-is-absolute": { 1320 | "version": "1.0.1", 1321 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1322 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1323 | "engines": { 1324 | "node": ">=0.10.0" 1325 | } 1326 | }, 1327 | "node_modules/tar": { 1328 | "version": "6.1.13", 1329 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", 1330 | "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", 1331 | "dependencies": { 1332 | "chownr": "^2.0.0", 1333 | "fs-minipass": "^2.0.0", 1334 | "minipass": "^4.0.0", 1335 | "minizlib": "^2.1.1", 1336 | "mkdirp": "^1.0.3", 1337 | "yallist": "^4.0.0" 1338 | }, 1339 | "engines": { 1340 | "node": ">=10" 1341 | } 1342 | }, 1343 | "node_modules/chownr": { 1344 | "version": "2.0.0", 1345 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", 1346 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", 1347 | "engines": { 1348 | "node": ">=10" 1349 | } 1350 | }, 1351 | "node_modules/fs-minipass": { 1352 | "version": "2.1.0", 1353 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 1354 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 1355 | "dependencies": { 1356 | "minipass": "^3.0.0" 1357 | }, 1358 | "engines": { 1359 | "node": ">= 8" 1360 | } 1361 | }, 1362 | "node_modules/minipass": { 1363 | "version": "3.3.6", 1364 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", 1365 | "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 1366 | "dependencies": { 1367 | "yallist": "^4.0.0" 1368 | }, 1369 | "engines": { 1370 | "node": ">=8" 1371 | } 1372 | }, 1373 | "node_modules/tar/node_modules/minipass": { 1374 | "version": "4.2.1", 1375 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.1.tgz", 1376 | "integrity": "sha512-KS4CHIsDfOZetnT+u6fwxyFADXLamtkPxkGScmmtTW//MlRrImV+LtbmbJpLQ86Hw7km/utbfEfndhGBrfwvlA==", 1377 | "engines": { 1378 | "node": ">=8" 1379 | } 1380 | }, 1381 | "node_modules/minizlib": { 1382 | "version": "2.1.2", 1383 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", 1384 | "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", 1385 | "dependencies": { 1386 | "minipass": "^3.0.0", 1387 | "yallist": "^4.0.0" 1388 | }, 1389 | "engines": { 1390 | "node": ">= 8" 1391 | } 1392 | }, 1393 | "node_modules/mkdirp": { 1394 | "version": "1.0.4", 1395 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 1396 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", 1397 | "bin": { 1398 | "mkdirp": "bin/cmd.js" 1399 | }, 1400 | "engines": { 1401 | "node": ">=10" 1402 | } 1403 | }, 1404 | "node_modules/node-addon-api": { 1405 | "version": "4.3.0", 1406 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", 1407 | "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" 1408 | }, 1409 | "node_modules/node-gyp": { 1410 | "version": "8.4.1", 1411 | "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", 1412 | "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", 1413 | "optional": true, 1414 | "dependencies": { 1415 | "env-paths": "^2.2.0", 1416 | "glob": "^7.1.4", 1417 | "graceful-fs": "^4.2.6", 1418 | "make-fetch-happen": "^9.1.0", 1419 | "nopt": "^5.0.0", 1420 | "npmlog": "^6.0.0", 1421 | "rimraf": "^3.0.2", 1422 | "semver": "^7.3.5", 1423 | "tar": "^6.1.2", 1424 | "which": "^2.0.2" 1425 | }, 1426 | "bin": { 1427 | "node-gyp": "bin/node-gyp.js" 1428 | }, 1429 | "engines": { 1430 | "node": ">= 10.12.0" 1431 | } 1432 | }, 1433 | "node_modules/env-paths": { 1434 | "version": "2.2.1", 1435 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", 1436 | "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", 1437 | "optional": true, 1438 | "engines": { 1439 | "node": ">=6" 1440 | } 1441 | }, 1442 | "node_modules/graceful-fs": { 1443 | "version": "4.2.10", 1444 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", 1445 | "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", 1446 | "devOptional": true 1447 | }, 1448 | "node_modules/make-fetch-happen": { 1449 | "version": "9.1.0", 1450 | "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", 1451 | "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", 1452 | "optional": true, 1453 | "dependencies": { 1454 | "agentkeepalive": "^4.1.3", 1455 | "cacache": "^15.2.0", 1456 | "http-cache-semantics": "^4.1.0", 1457 | "http-proxy-agent": "^4.0.1", 1458 | "https-proxy-agent": "^5.0.0", 1459 | "is-lambda": "^1.0.1", 1460 | "lru-cache": "^6.0.0", 1461 | "minipass": "^3.1.3", 1462 | "minipass-collect": "^1.0.2", 1463 | "minipass-fetch": "^1.3.2", 1464 | "minipass-flush": "^1.0.5", 1465 | "minipass-pipeline": "^1.2.4", 1466 | "negotiator": "^0.6.2", 1467 | "promise-retry": "^2.0.1", 1468 | "socks-proxy-agent": "^6.0.0", 1469 | "ssri": "^8.0.0" 1470 | }, 1471 | "engines": { 1472 | "node": ">= 10" 1473 | } 1474 | }, 1475 | "node_modules/agentkeepalive": { 1476 | "version": "4.2.1", 1477 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", 1478 | "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", 1479 | "optional": true, 1480 | "dependencies": { 1481 | "debug": "^4.1.0", 1482 | "depd": "^1.1.2", 1483 | "humanize-ms": "^1.2.1" 1484 | }, 1485 | "engines": { 1486 | "node": ">= 8.0.0" 1487 | } 1488 | }, 1489 | "node_modules/depd": { 1490 | "version": "1.1.2", 1491 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 1492 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", 1493 | "optional": true, 1494 | "engines": { 1495 | "node": ">= 0.6" 1496 | } 1497 | }, 1498 | "node_modules/humanize-ms": { 1499 | "version": "1.2.1", 1500 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 1501 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 1502 | "optional": true, 1503 | "dependencies": { 1504 | "ms": "^2.0.0" 1505 | } 1506 | }, 1507 | "node_modules/cacache": { 1508 | "version": "15.3.0", 1509 | "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", 1510 | "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", 1511 | "optional": true, 1512 | "dependencies": { 1513 | "@npmcli/fs": "^1.0.0", 1514 | "@npmcli/move-file": "^1.0.1", 1515 | "chownr": "^2.0.0", 1516 | "fs-minipass": "^2.0.0", 1517 | "glob": "^7.1.4", 1518 | "infer-owner": "^1.0.4", 1519 | "lru-cache": "^6.0.0", 1520 | "minipass": "^3.1.1", 1521 | "minipass-collect": "^1.0.2", 1522 | "minipass-flush": "^1.0.5", 1523 | "minipass-pipeline": "^1.2.2", 1524 | "mkdirp": "^1.0.3", 1525 | "p-map": "^4.0.0", 1526 | "promise-inflight": "^1.0.1", 1527 | "rimraf": "^3.0.2", 1528 | "ssri": "^8.0.1", 1529 | "tar": "^6.0.2", 1530 | "unique-filename": "^1.1.1" 1531 | }, 1532 | "engines": { 1533 | "node": ">= 10" 1534 | } 1535 | }, 1536 | "node_modules/@npmcli/fs": { 1537 | "version": "1.1.1", 1538 | "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", 1539 | "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", 1540 | "optional": true, 1541 | "dependencies": { 1542 | "@gar/promisify": "^1.0.1", 1543 | "semver": "^7.3.5" 1544 | } 1545 | }, 1546 | "node_modules/@gar/promisify": { 1547 | "version": "1.1.3", 1548 | "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", 1549 | "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", 1550 | "optional": true 1551 | }, 1552 | "node_modules/@npmcli/move-file": { 1553 | "version": "1.1.2", 1554 | "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", 1555 | "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", 1556 | "deprecated": "This functionality has been moved to @npmcli/fs", 1557 | "optional": true, 1558 | "dependencies": { 1559 | "mkdirp": "^1.0.4", 1560 | "rimraf": "^3.0.2" 1561 | }, 1562 | "engines": { 1563 | "node": ">=10" 1564 | } 1565 | }, 1566 | "node_modules/infer-owner": { 1567 | "version": "1.0.4", 1568 | "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", 1569 | "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", 1570 | "optional": true 1571 | }, 1572 | "node_modules/minipass-collect": { 1573 | "version": "1.0.2", 1574 | "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", 1575 | "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", 1576 | "optional": true, 1577 | "dependencies": { 1578 | "minipass": "^3.0.0" 1579 | }, 1580 | "engines": { 1581 | "node": ">= 8" 1582 | } 1583 | }, 1584 | "node_modules/minipass-flush": { 1585 | "version": "1.0.5", 1586 | "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", 1587 | "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", 1588 | "optional": true, 1589 | "dependencies": { 1590 | "minipass": "^3.0.0" 1591 | }, 1592 | "engines": { 1593 | "node": ">= 8" 1594 | } 1595 | }, 1596 | "node_modules/minipass-pipeline": { 1597 | "version": "1.2.4", 1598 | "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", 1599 | "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", 1600 | "optional": true, 1601 | "dependencies": { 1602 | "minipass": "^3.0.0" 1603 | }, 1604 | "engines": { 1605 | "node": ">=8" 1606 | } 1607 | }, 1608 | "node_modules/p-map": { 1609 | "version": "4.0.0", 1610 | "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", 1611 | "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", 1612 | "optional": true, 1613 | "dependencies": { 1614 | "aggregate-error": "^3.0.0" 1615 | }, 1616 | "engines": { 1617 | "node": ">=10" 1618 | }, 1619 | "funding": { 1620 | "url": "https://github.com/sponsors/sindresorhus" 1621 | } 1622 | }, 1623 | "node_modules/aggregate-error": { 1624 | "version": "3.1.0", 1625 | "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", 1626 | "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", 1627 | "optional": true, 1628 | "dependencies": { 1629 | "clean-stack": "^2.0.0", 1630 | "indent-string": "^4.0.0" 1631 | }, 1632 | "engines": { 1633 | "node": ">=8" 1634 | } 1635 | }, 1636 | "node_modules/clean-stack": { 1637 | "version": "2.2.0", 1638 | "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", 1639 | "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", 1640 | "optional": true, 1641 | "engines": { 1642 | "node": ">=6" 1643 | } 1644 | }, 1645 | "node_modules/indent-string": { 1646 | "version": "4.0.0", 1647 | "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", 1648 | "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", 1649 | "optional": true, 1650 | "engines": { 1651 | "node": ">=8" 1652 | } 1653 | }, 1654 | "node_modules/promise-inflight": { 1655 | "version": "1.0.1", 1656 | "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", 1657 | "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", 1658 | "optional": true 1659 | }, 1660 | "node_modules/ssri": { 1661 | "version": "8.0.1", 1662 | "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", 1663 | "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", 1664 | "optional": true, 1665 | "dependencies": { 1666 | "minipass": "^3.1.1" 1667 | }, 1668 | "engines": { 1669 | "node": ">= 8" 1670 | } 1671 | }, 1672 | "node_modules/unique-filename": { 1673 | "version": "1.1.1", 1674 | "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", 1675 | "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", 1676 | "optional": true, 1677 | "dependencies": { 1678 | "unique-slug": "^2.0.0" 1679 | } 1680 | }, 1681 | "node_modules/unique-slug": { 1682 | "version": "2.0.2", 1683 | "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", 1684 | "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", 1685 | "optional": true, 1686 | "dependencies": { 1687 | "imurmurhash": "^0.1.4" 1688 | } 1689 | }, 1690 | "node_modules/imurmurhash": { 1691 | "version": "0.1.4", 1692 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1693 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 1694 | "optional": true, 1695 | "engines": { 1696 | "node": ">=0.8.19" 1697 | } 1698 | }, 1699 | "node_modules/http-cache-semantics": { 1700 | "version": "4.1.1", 1701 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", 1702 | "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", 1703 | "optional": true 1704 | }, 1705 | "node_modules/http-proxy-agent": { 1706 | "version": "4.0.1", 1707 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", 1708 | "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", 1709 | "optional": true, 1710 | "dependencies": { 1711 | "@tootallnate/once": "1", 1712 | "agent-base": "6", 1713 | "debug": "4" 1714 | }, 1715 | "engines": { 1716 | "node": ">= 6" 1717 | } 1718 | }, 1719 | "node_modules/@tootallnate/once": { 1720 | "version": "1.1.2", 1721 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", 1722 | "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", 1723 | "optional": true, 1724 | "engines": { 1725 | "node": ">= 6" 1726 | } 1727 | }, 1728 | "node_modules/is-lambda": { 1729 | "version": "1.0.1", 1730 | "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", 1731 | "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", 1732 | "optional": true 1733 | }, 1734 | "node_modules/minipass-fetch": { 1735 | "version": "1.4.1", 1736 | "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", 1737 | "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", 1738 | "optional": true, 1739 | "dependencies": { 1740 | "minipass": "^3.1.0", 1741 | "minipass-sized": "^1.0.3", 1742 | "minizlib": "^2.0.0" 1743 | }, 1744 | "engines": { 1745 | "node": ">=8" 1746 | }, 1747 | "optionalDependencies": { 1748 | "encoding": "^0.1.12" 1749 | } 1750 | }, 1751 | "node_modules/minipass-sized": { 1752 | "version": "1.0.3", 1753 | "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", 1754 | "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", 1755 | "optional": true, 1756 | "dependencies": { 1757 | "minipass": "^3.0.0" 1758 | }, 1759 | "engines": { 1760 | "node": ">=8" 1761 | } 1762 | }, 1763 | "node_modules/negotiator": { 1764 | "version": "0.6.3", 1765 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1766 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1767 | "optional": true, 1768 | "engines": { 1769 | "node": ">= 0.6" 1770 | } 1771 | }, 1772 | "node_modules/promise-retry": { 1773 | "version": "2.0.1", 1774 | "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", 1775 | "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", 1776 | "optional": true, 1777 | "dependencies": { 1778 | "err-code": "^2.0.2", 1779 | "retry": "^0.12.0" 1780 | }, 1781 | "engines": { 1782 | "node": ">=10" 1783 | } 1784 | }, 1785 | "node_modules/err-code": { 1786 | "version": "2.0.3", 1787 | "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", 1788 | "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", 1789 | "optional": true 1790 | }, 1791 | "node_modules/retry": { 1792 | "version": "0.12.0", 1793 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 1794 | "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", 1795 | "optional": true, 1796 | "engines": { 1797 | "node": ">= 4" 1798 | } 1799 | }, 1800 | "node_modules/socks-proxy-agent": { 1801 | "version": "6.2.1", 1802 | "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", 1803 | "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", 1804 | "optional": true, 1805 | "dependencies": { 1806 | "agent-base": "^6.0.2", 1807 | "debug": "^4.3.3", 1808 | "socks": "^2.6.2" 1809 | }, 1810 | "engines": { 1811 | "node": ">= 10" 1812 | } 1813 | }, 1814 | "node_modules/socks": { 1815 | "version": "2.7.1", 1816 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", 1817 | "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", 1818 | "optional": true, 1819 | "dependencies": { 1820 | "ip": "^2.0.0", 1821 | "smart-buffer": "^4.2.0" 1822 | }, 1823 | "engines": { 1824 | "node": ">= 10.13.0", 1825 | "npm": ">= 3.0.0" 1826 | } 1827 | }, 1828 | "node_modules/ip": { 1829 | "version": "2.0.0", 1830 | "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", 1831 | "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", 1832 | "optional": true 1833 | }, 1834 | "node_modules/smart-buffer": { 1835 | "version": "4.2.0", 1836 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 1837 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", 1838 | "optional": true, 1839 | "engines": { 1840 | "node": ">= 6.0.0", 1841 | "npm": ">= 3.0.0" 1842 | } 1843 | }, 1844 | "node_modules/node-gyp/node_modules/npmlog": { 1845 | "version": "6.0.2", 1846 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", 1847 | "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", 1848 | "optional": true, 1849 | "dependencies": { 1850 | "are-we-there-yet": "^3.0.0", 1851 | "console-control-strings": "^1.1.0", 1852 | "gauge": "^4.0.3", 1853 | "set-blocking": "^2.0.0" 1854 | }, 1855 | "engines": { 1856 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 1857 | } 1858 | }, 1859 | "node_modules/node-gyp/node_modules/are-we-there-yet": { 1860 | "version": "3.0.1", 1861 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", 1862 | "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", 1863 | "optional": true, 1864 | "dependencies": { 1865 | "delegates": "^1.0.0", 1866 | "readable-stream": "^3.6.0" 1867 | }, 1868 | "engines": { 1869 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 1870 | } 1871 | }, 1872 | "node_modules/node-gyp/node_modules/readable-stream": { 1873 | "version": "3.6.1", 1874 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", 1875 | "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", 1876 | "optional": true, 1877 | "dependencies": { 1878 | "inherits": "^2.0.3", 1879 | "string_decoder": "^1.1.1", 1880 | "util-deprecate": "^1.0.1" 1881 | }, 1882 | "engines": { 1883 | "node": ">= 6" 1884 | } 1885 | }, 1886 | "node_modules/node-gyp/node_modules/gauge": { 1887 | "version": "4.0.4", 1888 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", 1889 | "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", 1890 | "optional": true, 1891 | "dependencies": { 1892 | "aproba": "^1.0.3 || ^2.0.0", 1893 | "color-support": "^1.1.3", 1894 | "console-control-strings": "^1.1.0", 1895 | "has-unicode": "^2.0.1", 1896 | "signal-exit": "^3.0.7", 1897 | "string-width": "^4.2.3", 1898 | "strip-ansi": "^6.0.1", 1899 | "wide-align": "^1.1.5" 1900 | }, 1901 | "engines": { 1902 | "node": "^12.13.0 || ^14.15.0 || >=16.0.0" 1903 | } 1904 | }, 1905 | "node_modules/which": { 1906 | "version": "2.0.2", 1907 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1908 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1909 | "optional": true, 1910 | "dependencies": { 1911 | "isexe": "^2.0.0" 1912 | }, 1913 | "bin": { 1914 | "node-which": "bin/node-which" 1915 | }, 1916 | "engines": { 1917 | "node": ">= 8" 1918 | } 1919 | }, 1920 | "node_modules/isexe": { 1921 | "version": "2.0.0", 1922 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1923 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1924 | "optional": true 1925 | }, 1926 | "node_modules/sqlstring-sqlite": { 1927 | "version": "0.1.1", 1928 | "license": "MIT", 1929 | "engines": { 1930 | "node": ">= 0.6" 1931 | } 1932 | }, 1933 | "node_modules/@types/sqlite3": { 1934 | "version": "3.1.8", 1935 | "dev": true, 1936 | "license": "MIT", 1937 | "dependencies": { 1938 | "@types/node": "*" 1939 | } 1940 | }, 1941 | "node_modules/@types/xml2js": { 1942 | "version": "0.4.11", 1943 | "dev": true, 1944 | "license": "MIT", 1945 | "dependencies": { 1946 | "@types/node": "*" 1947 | } 1948 | }, 1949 | "node_modules/ts-node": { 1950 | "version": "10.9.1", 1951 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 1952 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 1953 | "dev": true, 1954 | "dependencies": { 1955 | "@cspotcode/source-map-support": "^0.8.0", 1956 | "@tsconfig/node10": "^1.0.7", 1957 | "@tsconfig/node12": "^1.0.7", 1958 | "@tsconfig/node14": "^1.0.0", 1959 | "@tsconfig/node16": "^1.0.2", 1960 | "acorn": "^8.4.1", 1961 | "acorn-walk": "^8.1.1", 1962 | "arg": "^4.1.0", 1963 | "create-require": "^1.1.0", 1964 | "diff": "^4.0.1", 1965 | "make-error": "^1.1.1", 1966 | "v8-compile-cache-lib": "^3.0.1", 1967 | "yn": "3.1.1" 1968 | }, 1969 | "bin": { 1970 | "ts-node": "dist/bin.js", 1971 | "ts-node-cwd": "dist/bin-cwd.js", 1972 | "ts-node-esm": "dist/bin-esm.js", 1973 | "ts-node-script": "dist/bin-script.js", 1974 | "ts-node-transpile-only": "dist/bin-transpile.js", 1975 | "ts-script": "dist/bin-script-deprecated.js" 1976 | }, 1977 | "peerDependencies": { 1978 | "@swc/core": ">=1.2.50", 1979 | "@swc/wasm": ">=1.2.50", 1980 | "@types/node": "*", 1981 | "typescript": ">=2.7" 1982 | }, 1983 | "peerDependenciesMeta": { 1984 | "@swc/core": { 1985 | "optional": true 1986 | }, 1987 | "@swc/wasm": { 1988 | "optional": true 1989 | } 1990 | } 1991 | }, 1992 | "node_modules/@cspotcode/source-map-support": { 1993 | "version": "0.8.1", 1994 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 1995 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 1996 | "dev": true, 1997 | "dependencies": { 1998 | "@jridgewell/trace-mapping": "0.3.9" 1999 | }, 2000 | "engines": { 2001 | "node": ">=12" 2002 | } 2003 | }, 2004 | "node_modules/@jridgewell/trace-mapping": { 2005 | "version": "0.3.9", 2006 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 2007 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 2008 | "dev": true, 2009 | "dependencies": { 2010 | "@jridgewell/resolve-uri": "^3.0.3", 2011 | "@jridgewell/sourcemap-codec": "^1.4.10" 2012 | } 2013 | }, 2014 | "node_modules/@jridgewell/resolve-uri": { 2015 | "version": "3.1.0", 2016 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", 2017 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", 2018 | "dev": true, 2019 | "engines": { 2020 | "node": ">=6.0.0" 2021 | } 2022 | }, 2023 | "node_modules/@jridgewell/sourcemap-codec": { 2024 | "version": "1.4.14", 2025 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", 2026 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", 2027 | "dev": true 2028 | }, 2029 | "node_modules/@tsconfig/node10": { 2030 | "version": "1.0.9", 2031 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 2032 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 2033 | "dev": true 2034 | }, 2035 | "node_modules/@tsconfig/node12": { 2036 | "version": "1.0.11", 2037 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 2038 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 2039 | "dev": true 2040 | }, 2041 | "node_modules/@tsconfig/node14": { 2042 | "version": "1.0.3", 2043 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 2044 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 2045 | "dev": true 2046 | }, 2047 | "node_modules/acorn": { 2048 | "version": "8.8.0", 2049 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", 2050 | "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", 2051 | "dev": true, 2052 | "bin": { 2053 | "acorn": "bin/acorn" 2054 | }, 2055 | "engines": { 2056 | "node": ">=0.4.0" 2057 | } 2058 | }, 2059 | "node_modules/acorn-walk": { 2060 | "version": "8.2.0", 2061 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 2062 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 2063 | "dev": true, 2064 | "engines": { 2065 | "node": ">=0.4.0" 2066 | } 2067 | }, 2068 | "node_modules/arg": { 2069 | "version": "4.1.3", 2070 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 2071 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 2072 | "dev": true 2073 | }, 2074 | "node_modules/create-require": { 2075 | "version": "1.1.1", 2076 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 2077 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 2078 | "dev": true 2079 | }, 2080 | "node_modules/diff": { 2081 | "version": "4.0.2", 2082 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 2083 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 2084 | "dev": true, 2085 | "engines": { 2086 | "node": ">=0.3.1" 2087 | } 2088 | }, 2089 | "node_modules/make-error": { 2090 | "version": "1.3.6", 2091 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 2092 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 2093 | "dev": true 2094 | }, 2095 | "node_modules/v8-compile-cache-lib": { 2096 | "version": "3.0.1", 2097 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 2098 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 2099 | "dev": true 2100 | }, 2101 | "node_modules/yn": { 2102 | "version": "3.1.1", 2103 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 2104 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 2105 | "dev": true, 2106 | "engines": { 2107 | "node": ">=6" 2108 | } 2109 | } 2110 | } 2111 | } 2112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hasura/dc-agent-sqlite", 3 | "version": "0.1.0", 4 | "description": "SQLite Data Connector Agent for Hasura GraphQL Engine", 5 | "author": "Hasura (https://github.com/hasura/graphql-engine)", 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/hasura/graphql-engine.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/hasura/graphql-engine/issues" 13 | }, 14 | "homepage": "https://github.com/hasura/graphql-engine#readme", 15 | "main": "dist/index.js", 16 | "scripts": { 17 | "build": "tsc", 18 | "typecheck": "tsc --noEmit", 19 | "start": "ts-node ./src/index.ts", 20 | "start-no-typecheck": "ts-node --transpileOnly ./src/index.ts", 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "dependencies": { 24 | "@fastify/cors": "^8.1.0", 25 | "@hasura/dc-api-types": "0.45.0", 26 | "fastify-metrics": "^9.2.1", 27 | "fastify": "^4.13.0", 28 | "nanoid": "^3.3.4", 29 | "openapi3-ts": "^2.0.2", 30 | "pino-pretty": "^8.1.0", 31 | "sqlite-parser": "^1.0.1", 32 | "sqlite3": "^5.1.4", 33 | "sqlstring-sqlite": "^0.1.1" 34 | }, 35 | "devDependencies": { 36 | "@tsconfig/node16": "^1.0.3", 37 | "@types/node": "^16.11.49", 38 | "@types/sqlite3": "^3.1.8", 39 | "@types/xml2js": "^0.4.11", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^4.7.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/capabilities.ts: -------------------------------------------------------------------------------- 1 | import { configSchema } from "./config" 2 | import { DATASETS, METRICS, MUTATIONS } from "./environment" 3 | 4 | import { CapabilitiesResponse, ScalarTypeCapabilities } from "@hasura/dc-api-types" 5 | 6 | // NOTE: This should cover all possible schema types. 7 | // This type should be a subtype of ScalarType. 8 | export type ScalarTypeKey 9 | = 'DateTime' 10 | | 'string' 11 | | 'number' 12 | | 'decimal' 13 | | 'bool' 14 | ; 15 | 16 | // TODO: How can we ensure that we have covered all of the operator keys in the query module? 17 | const scalar_types: Record = { 18 | DateTime: { 19 | comparison_operators: { 20 | _in_year: 'int' 21 | }, 22 | graphql_type: "String", 23 | }, 24 | string: { 25 | comparison_operators: { 26 | // See: https://www.sqlite.org/lang_expr.html #5 27 | _like: 'string', 28 | _glob: 'string', 29 | // _regexp: 'string', // TODO: Detect if REGEXP is supported 30 | }, 31 | aggregate_functions: { 32 | max: 'string', 33 | min: 'string' 34 | }, 35 | graphql_type: "String" 36 | }, 37 | // TODO: Why do we need a seperate 'decimal' type? 38 | decimal: { 39 | comparison_operators: { 40 | _modulus_is_zero: 'decimal', 41 | }, 42 | aggregate_functions: { 43 | max: 'decimal', 44 | min: 'decimal', 45 | sum: 'decimal' 46 | }, 47 | update_column_operators: { 48 | inc: { 49 | argument_type: 'decimal' 50 | }, 51 | dec: { 52 | argument_type: 'decimal' 53 | } 54 | }, 55 | graphql_type: "Float" 56 | }, 57 | number: { 58 | comparison_operators: { 59 | _modulus_is_zero: 'number', 60 | }, 61 | aggregate_functions: { 62 | max: 'number', 63 | min: 'number', 64 | sum: 'number' 65 | }, 66 | update_column_operators: { 67 | inc: { 68 | argument_type: 'number' 69 | }, 70 | dec: { 71 | argument_type: 'number' 72 | } 73 | }, 74 | graphql_type: "Float" 75 | }, 76 | bool: { 77 | comparison_operators: { 78 | _and: 'bool', 79 | _or: 'bool', 80 | _nand: 'bool', 81 | _xor: 'bool', 82 | }, 83 | graphql_type: "Boolean" 84 | } 85 | }; 86 | 87 | export const capabilitiesResponse: CapabilitiesResponse = { 88 | display_name: 'Hasura SQLite', 89 | // release_name: 'Beta', 90 | config_schemas: configSchema, 91 | capabilities: { 92 | data_schema: { 93 | supports_primary_keys: true, 94 | supports_foreign_keys: true, 95 | column_nullability: "nullable_and_non_nullable", 96 | }, 97 | post_schema: {}, 98 | scalar_types, 99 | queries: { 100 | foreach: {} 101 | }, 102 | relationships: {}, 103 | interpolated_queries: {}, 104 | comparisons: { 105 | subquery: { 106 | supports_relations: true 107 | } 108 | }, 109 | ... ( 110 | MUTATIONS 111 | ? { 112 | mutations: { 113 | atomicity_support_level: "heterogeneous_operations", 114 | insert: { supports_nested_inserts: true }, 115 | update: {}, 116 | delete: {}, 117 | returning: {}, 118 | } 119 | } 120 | : {} 121 | ), 122 | explain: {}, 123 | raw: {}, 124 | ... (DATASETS ? { datasets: {} } : {}), 125 | ... (METRICS ? { metrics: {} } : {}) 126 | }, 127 | } 128 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { FastifyRequest } from "fastify" 2 | import { ConfigSchemaResponse } from "@hasura/dc-api-types" 3 | 4 | export type Config = { 5 | db: string, 6 | explicit_main_schema: Boolean, 7 | tables: String[] | null, 8 | meta: Boolean 9 | } 10 | 11 | export const getConfig = (request: FastifyRequest): Config => { 12 | const config = tryGetConfig(request); 13 | if (config === null) { 14 | throw new Error("X-Hasura-DataConnector-Config header must specify db"); 15 | } 16 | return config; 17 | } 18 | 19 | export const tryGetConfig = (request: FastifyRequest): Config | null => { 20 | const configHeader = request.headers["x-hasura-dataconnector-config"]; 21 | const rawConfigJson = Array.isArray(configHeader) ? configHeader[0] : configHeader ?? "{}"; 22 | const config = JSON.parse(rawConfigJson); 23 | 24 | if(config.db == null) { 25 | return null; 26 | } 27 | 28 | return { 29 | db: config.db, 30 | explicit_main_schema: config.explicit_main_schema ?? false, 31 | tables: config.tables ?? null, 32 | meta: config.include_sqlite_meta_tables ?? false 33 | } 34 | } 35 | 36 | export const configSchema: ConfigSchemaResponse = { 37 | config_schema: { 38 | type: "object", 39 | nullable: false, 40 | required: ["db"], 41 | properties: { 42 | db: { 43 | description: "The SQLite database file to use.", 44 | type: "string" 45 | }, 46 | explicit_main_schema: { 47 | description: "Prefix all tables with the 'main' schema", 48 | type: "boolean", 49 | nullable: true, 50 | default: false 51 | }, 52 | tables: { 53 | description: "List of tables to make available in the schema and for querying", 54 | type: "array", 55 | items: { $ref: "#/other_schemas/TableName" }, 56 | nullable: true 57 | }, 58 | include_sqlite_meta_tables: { 59 | description: "By default index tables, etc are not included, set this to true to include them.", 60 | type: "boolean", 61 | nullable: true 62 | }, 63 | DEBUG: { 64 | description: "For debugging.", 65 | type: "object", 66 | additionalProperties: true, 67 | nullable: true 68 | } 69 | } 70 | }, 71 | other_schemas: { 72 | TableName: { 73 | nullable: false, 74 | type: "string" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/datasets.ts: -------------------------------------------------------------------------------- 1 | import { createDbMode, defaultMode, SqlLogger, withConnection } from './db'; 2 | import { DatasetDeleteCloneResponse, DatasetGetTemplateResponse, DatasetCreateCloneRequest, DatasetCreateCloneResponse, } from '@hasura/dc-api-types'; 3 | import { access, constants, promises, existsSync } from 'fs'; 4 | import { DATASET_CLONES, DATASET_DELETE, DATASET_TEMPLATES } from "./environment"; 5 | import path from 'path'; 6 | 7 | export async function getDataset(template_name: string): Promise { 8 | const templatePaths = mkTemplatePaths(template_name); 9 | return { 10 | exists: await fileIsReadable(templatePaths.dbFileTemplatePath) || await fileIsReadable(templatePaths.sqlFileTemplatePath) 11 | }; 12 | } 13 | 14 | export async function cloneDataset(logger: SqlLogger, clone_name: string, body: DatasetCreateCloneRequest): Promise { 15 | const templatePaths = mkTemplatePaths(body.from); 16 | const toPath = mkClonePath(clone_name); 17 | const cloneExistsAlready = await fileIsReadable(toPath); 18 | if (cloneExistsAlready) { 19 | throw new Error("Dataset clone already exists"); 20 | } 21 | 22 | if (await fileIsReadable(templatePaths.dbFileTemplatePath)) { 23 | try { 24 | await withConnection({ db: templatePaths.dbFileTemplatePath, explicit_main_schema: false, tables: [], meta: false }, defaultMode, logger, async db => {}); 25 | } 26 | catch { 27 | throw new Error("Dataset template is not a valid SQLite database!"); 28 | } 29 | 30 | await promises.cp(templatePaths.dbFileTemplatePath, toPath); 31 | return { config: { db: toPath } }; 32 | 33 | } else if (await fileIsReadable(templatePaths.sqlFileTemplatePath)) { 34 | const sql = await promises.readFile(templatePaths.sqlFileTemplatePath, { encoding: "utf-8" }); 35 | await withConnection({db: toPath, explicit_main_schema: false, tables: [], meta: false}, createDbMode, logger, async db => { 36 | await db.withTransaction(async () => { 37 | await db.exec(sql); 38 | }); 39 | }); 40 | return { config: { db: toPath } }; 41 | } else { 42 | throw new Error("Dataset template does not exist"); 43 | } 44 | } 45 | 46 | export async function deleteDataset(clone_name: string): Promise { 47 | if(DATASET_DELETE) { 48 | const path = mkClonePath(clone_name); 49 | const exists = existsSync(path); 50 | if(exists) { 51 | const stats = await promises.stat(path); 52 | if(stats.isFile()) { 53 | await promises.rm(path); 54 | return { message: "success" }; 55 | } else { 56 | throw(Error("Dataset is not a file.")); 57 | } 58 | } else { 59 | throw(Error("Dataset does not exist.")); 60 | } 61 | } else { 62 | throw(Error("Dataset deletion not available.")); 63 | } 64 | } 65 | 66 | type TemplatePaths = { 67 | dbFileTemplatePath: string, 68 | sqlFileTemplatePath: string, 69 | } 70 | 71 | function mkTemplatePaths(name: string): TemplatePaths { 72 | const parsed = path.parse(name); 73 | const safeName = parsed.base; 74 | if(name != safeName) { 75 | throw(Error(`Template name ${name} is not valid.`)); 76 | } 77 | return { 78 | dbFileTemplatePath: path.join(DATASET_TEMPLATES, safeName + ".sqlite"), 79 | sqlFileTemplatePath: path.join(DATASET_TEMPLATES, safeName + ".sql"), 80 | }; 81 | } 82 | 83 | function mkClonePath(name: string): string { 84 | const parsed = path.parse(name); 85 | const safeName = parsed.base; 86 | if(name != safeName) { 87 | throw(Error(`Template name ${name} is not valid.`)); 88 | } 89 | return path.join(DATASET_CLONES, safeName + ".sqlite"); 90 | } 91 | 92 | export const fileIsReadable = async(filepath: string): Promise => { 93 | return new Promise((resolve) => { 94 | access(filepath, constants.R_OK, err => err ? resolve(false) : resolve(true)); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config"; 2 | import { DB_ALLOW_LIST, DB_CREATE, DB_PRIVATECACHE, DB_READONLY } from "./environment"; 3 | import SQLite from 'sqlite3'; 4 | 5 | export type SqlLogger = (sql: string) => void 6 | 7 | // See https://github.com/TryGhost/node-sqlite3/wiki/API#new-sqlite3databasefilename--mode--callback 8 | // mode (optional): One or more of OPEN_READONLY | OPEN_READWRITE | OPEN_CREATE | OPEN_FULLMUTEX | OPEN_URI | OPEN_SHAREDCACHE | OPEN_PRIVATECACHE 9 | // The default value is OPEN_READWRITE | OPEN_CREATE | OPEN_FULLMUTEX. 10 | const readMode = DB_READONLY ? SQLite.OPEN_READONLY : SQLite.OPEN_READWRITE; 11 | const createMode = DB_CREATE ? SQLite.OPEN_CREATE : 0; // Flag style means 0=off 12 | const cacheMode = DB_PRIVATECACHE ? SQLite.OPEN_PRIVATECACHE : SQLite.OPEN_SHAREDCACHE; 13 | export const defaultMode = readMode | createMode | cacheMode; 14 | export const createDbMode = SQLite.OPEN_CREATE | readMode | cacheMode; 15 | 16 | export type Connection = { 17 | query: (query: string, params?: Record) => Promise>, 18 | exec: (sql: string) => Promise; 19 | withTransaction: (action: () => Promise) => Promise 20 | } 21 | 22 | export async function withConnection(config: Config, mode: number, sqlLogger: SqlLogger, useConnection: (connection: Connection) => Promise): Promise { 23 | if(DB_ALLOW_LIST != null) { 24 | if(DB_ALLOW_LIST.includes(config.db)) { 25 | throw new Error(`Database ${config.db} is not present in DB_ALLOW_LIST 😭`); 26 | } 27 | } 28 | 29 | const db_ = await new Promise((resolve, reject) => { 30 | const db = new SQLite.Database(config.db, mode, err => { 31 | if (err) { 32 | reject(err); 33 | } else { 34 | resolve(db); 35 | } 36 | }); 37 | }); 38 | 39 | // NOTE: Avoiding util.promisify as this seems to be causing connection failures. 40 | const query = (query: string, params?: Record): Promise> => { 41 | return new Promise((resolve, reject) => { 42 | /* Pass named params: 43 | * db.run("UPDATE tbl SET name = $name WHERE id = $id", { 44 | * $id: 2, 45 | * $name: "bar" 46 | * }); 47 | */ 48 | sqlLogger(query); 49 | db_.all(query, params || {}, (err, data) => { 50 | if (err) { 51 | return reject(err); 52 | } else { 53 | resolve(data); 54 | } 55 | }) 56 | }) 57 | } 58 | 59 | const exec = (sql: string): Promise => { 60 | return new Promise((resolve, reject) => { 61 | sqlLogger(sql); 62 | db_.exec(sql, err => { 63 | if (err) { 64 | reject(err); 65 | } else { 66 | resolve(); 67 | } 68 | }) 69 | }) 70 | }; 71 | 72 | const withTransaction = async (action: () => Promise): Promise => { 73 | await exec("BEGIN TRANSACTION"); 74 | try { 75 | const result = await action(); 76 | await exec("COMMIT"); 77 | return result; 78 | } catch (err) { 79 | await exec("ROLLBACK") 80 | throw err; 81 | } 82 | } 83 | 84 | try { 85 | return await useConnection({ query, exec, withTransaction }); 86 | } 87 | finally { 88 | await new Promise((resolve, reject) => { 89 | db_.close((err) => { 90 | if (err) { 91 | return reject(err); 92 | } else { 93 | resolve(true); // What should we resolve with if there's no data to promise? 94 | } 95 | }) 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 |  2 | export function stringToBool(x: string | null | undefined): boolean { 3 | return (/1|true|t|yes|y/i).test(x || ''); 4 | } 5 | 6 | export function envToBool(envVarName: string): boolean { 7 | return stringToBool(process.env[envVarName]); 8 | } 9 | 10 | export function envToString(envVarName: string, defaultValue: string): string { 11 | const val = process.env[envVarName]; 12 | return val === undefined ? defaultValue : val; 13 | } 14 | 15 | export function envToNum(envVarName: string, defaultValue: number): number { 16 | const val = process.env[envVarName]; 17 | return val === undefined ? defaultValue : Number(val); 18 | } 19 | 20 | export function envToArrayOfString(envVarName: string, defaultValue: Array | null = null): Array | null { 21 | const val = process.env[envVarName]; 22 | return val == null ? defaultValue : val.split(','); 23 | } 24 | 25 | export const LOG_LEVEL = envToString("LOG_LEVEL", "info"); 26 | export const PRETTY_PRINT_LOGS = envToBool('PRETTY_PRINT_LOGS'); 27 | export const METRICS = envToBool('METRICS'); 28 | 29 | // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 30 | export const PERMISSIVE_CORS = envToBool('PERMISSIVE_CORS'); 31 | export const DB_ALLOW_LIST = envToArrayOfString('DB_ALLOW_LIST'); 32 | 33 | // The default value is OPEN_READWRITE | OPEN_CREATE | OPEN_FULLMUTEX. 34 | export const DB_READONLY = envToBool('DB_READONLY'); 35 | export const DB_CREATE = envToBool('DB_CREATE'); 36 | export const DB_PRIVATECACHE = envToBool('DB_PRIVATECACHE'); 37 | 38 | export const DEBUGGING_TAGS = envToBool('DEBUGGING_TAGS'); 39 | export const QUERY_LENGTH_LIMIT = envToNum('QUERY_LENGTH_LIMIT', Infinity); 40 | 41 | export const MUTATIONS = envToBool('MUTATIONS'); 42 | 43 | export const DATASETS = envToBool('DATASETS'); 44 | export const DATASET_DELETE = envToBool('DATASET_DELETE'); 45 | export const DATASET_TEMPLATES = envToString('DATASET_TEMPLATES', "./dataset_templates"); 46 | export const DATASET_CLONES = envToString('DATASET_CLONES', "./dataset_clones"); 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify'; 2 | import FastifyCors from '@fastify/cors'; 3 | import { getSchema } from './schema'; 4 | import { explain, queryData } from './query'; 5 | import { getConfig, tryGetConfig } from './config'; 6 | import { capabilitiesResponse } from './capabilities'; 7 | import { QueryResponse, SchemaResponse, QueryRequest, CapabilitiesResponse, ExplainResponse, RawRequest, RawResponse, ErrorResponse, MutationRequest, MutationResponse, DatasetTemplateName, DatasetGetTemplateResponse, DatasetCreateCloneRequest, DatasetCreateCloneResponse, DatasetDeleteCloneResponse, SchemaRequest } from '@hasura/dc-api-types'; 8 | import { defaultMode, withConnection } from './db'; 9 | import metrics from 'fastify-metrics'; 10 | import prometheus from 'prom-client'; 11 | import { runRawOperation } from './raw'; 12 | import { DATASETS, DATASET_DELETE, LOG_LEVEL, METRICS, MUTATIONS, PERMISSIVE_CORS, PRETTY_PRINT_LOGS } from './environment'; 13 | import { cloneDataset, deleteDataset, getDataset } from './datasets'; 14 | import { runMutation } from './mutation'; 15 | import { ErrorWithStatusCode, unreachable } from './util'; 16 | 17 | const port = Number(process.env.PORT) || 8100; 18 | 19 | // NOTE: Pretty printing for logs is no longer supported out of the box. 20 | // See: https://github.com/pinojs/pino-pretty#integration 21 | // Pretty printed logs will be enabled if you have the `pino-pretty` 22 | // dev dependency installed as per the package.json settings. 23 | const server = Fastify({ 24 | logger: 25 | { 26 | level: LOG_LEVEL, 27 | ...( 28 | PRETTY_PRINT_LOGS 29 | ? { transport: { target: 'pino-pretty' } } 30 | : {} 31 | ) 32 | } 33 | }) 34 | 35 | server.setErrorHandler(function (error, _request, reply) { 36 | // Log error 37 | this.log.error(error) 38 | 39 | if (error instanceof ErrorWithStatusCode) { 40 | const errorResponse: ErrorResponse = { 41 | type: error.type, 42 | message: error.message, 43 | details: error.details 44 | }; 45 | reply.status(error.code).send(errorResponse); 46 | 47 | } else { 48 | const errorResponse: ErrorResponse = { 49 | type: "uncaught-error", 50 | message: "SQLite Agent: Uncaught Exception", 51 | details: { 52 | name: error.name, 53 | message: error.message 54 | } 55 | }; 56 | reply.status(500).send(errorResponse); 57 | } 58 | }) 59 | 60 | if(METRICS) { 61 | // See: https://www.npmjs.com/package/fastify-metrics 62 | server.register(metrics, { 63 | endpoint: '/metrics', 64 | routeMetrics: { 65 | enabled: true, 66 | registeredRoutesOnly: false, 67 | } 68 | }); 69 | } 70 | 71 | if(PERMISSIVE_CORS) { 72 | // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 73 | server.register(FastifyCors, { 74 | origin: true, 75 | methods: ["GET", "POST", "OPTIONS"], 76 | allowedHeaders: ["X-Hasura-DataConnector-Config", "X-Hasura-DataConnector-SourceName"] 77 | }); 78 | } 79 | 80 | // Register request-hook metrics. 81 | // This is done in a closure so that the metrics are scoped here. 82 | (() => { 83 | if(! METRICS) { 84 | return; 85 | } 86 | 87 | const requestCounter = new prometheus.Counter({ 88 | name: 'http_request_count', 89 | help: 'Number of requests', 90 | labelNames: ['route'], 91 | }); 92 | 93 | // Register a global request counting metric 94 | // See: https://www.fastify.io/docs/latest/Reference/Hooks/#onrequest 95 | server.addHook('onRequest', async (request, reply) => { 96 | requestCounter.inc({route: request.routerPath}); 97 | }) 98 | })(); 99 | 100 | // This is a hack to get Fastify to parse bodies on /schema GET requests 101 | // We basically trick its code into thinking the request is actually a POST 102 | // request so it doesn't skip parsing request bodies. 103 | server.addHook("onRequest", async(request, reply) => { 104 | if (request.routerPath === "/schema") 105 | request.raw.method = "POST" 106 | }) 107 | 108 | // Serves as an example of a custom histogram 109 | // Not especially useful at present as this mirrors 110 | // http_request_duration_seconds_bucket but is less general 111 | // but the query endpoint will offer more statistics specific 112 | // to the database interactions in future. 113 | const queryHistogram = new prometheus.Histogram({ 114 | name: 'query_durations', 115 | help: 'Histogram of the duration of query response times.', 116 | buckets: prometheus.exponentialBuckets(0.0001, 10, 8), 117 | labelNames: ['route'] as const, 118 | }); 119 | 120 | const sqlLogger = (sql: string): void => { 121 | server.log.debug({sql}, "Executed SQL"); 122 | }; 123 | 124 | server.get<{ Reply: CapabilitiesResponse }>("/capabilities", async (request, _response) => { 125 | server.log.info({ headers: request.headers, query: request.body, }, "capabilities.request"); 126 | return capabilitiesResponse; 127 | }); 128 | 129 | server.post<{ Body: SchemaRequest | undefined, Reply: SchemaResponse }>("/schema", async (request, _response) => { 130 | server.log.info({ headers: request.headers, query: request.body, }, "schema.request"); 131 | const config = getConfig(request); 132 | return getSchema(config, sqlLogger, request.body); 133 | }); 134 | 135 | /** 136 | * @throws ErrorWithStatusCode 137 | */ 138 | server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (request, response) => { 139 | server.log.info({ headers: request.headers, query: request.body, }, "query.request"); 140 | const end = queryHistogram.startTimer() 141 | const config = getConfig(request); 142 | const body = request.body; 143 | switch(body.target.type) { 144 | case 'function': 145 | throw new ErrorWithStatusCode( 146 | "User defined functions not supported in queries", 147 | 500, 148 | {function: { name: body.target.name }} 149 | ); 150 | case 'interpolated': // interpolated should actually work identically to tables when using the CTE pattern 151 | case 'table': 152 | try { 153 | const result : QueryResponse = await queryData(config, sqlLogger, body); 154 | return result; 155 | } finally { 156 | end(); 157 | } 158 | } 159 | }); 160 | 161 | // TODO: Use derived types for body and reply 162 | server.post<{ Body: RawRequest, Reply: RawResponse }>("/raw", async (request, _response) => { 163 | server.log.info({ headers: request.headers, query: request.body, }, "schema.raw"); 164 | const config = getConfig(request); 165 | return runRawOperation(config, sqlLogger, request.body); 166 | }); 167 | 168 | server.post<{ Body: QueryRequest, Reply: ExplainResponse}>("/explain", async (request, _response) => { 169 | server.log.info({ headers: request.headers, query: request.body, }, "query.request"); 170 | const config = getConfig(request); 171 | const body = request.body; 172 | switch(body.target.type) { 173 | case 'function': 174 | throw new ErrorWithStatusCode( 175 | "User defined functions not supported in queries", 176 | 500, 177 | {function: { name: body.target.name }} 178 | ); 179 | case 'table': 180 | return explain(config, sqlLogger, body); 181 | case 'interpolated': 182 | return explain(config, sqlLogger, body); 183 | default: 184 | throw(unreachable); 185 | } 186 | }); 187 | 188 | if(MUTATIONS) { 189 | server.post<{ Body: MutationRequest, Reply: MutationResponse}>("/mutation", async (request, _response) => { 190 | server.log.info({ headers: request.headers, query: request.body, }, "mutation.request"); 191 | // TODO: Mutation Histogram? 192 | const config = getConfig(request); 193 | return runMutation(config, sqlLogger, request.body); 194 | }); 195 | } 196 | 197 | server.get("/health", async (request, response) => { 198 | const config = tryGetConfig(request); 199 | response.type('application/json'); 200 | 201 | if(config === null) { 202 | server.log.info({ headers: request.headers, query: request.body, }, "health.request"); 203 | response.statusCode = 204; 204 | } else { 205 | server.log.info({ headers: request.headers, query: request.body, }, "health.db.request"); 206 | return await withConnection(config, defaultMode, sqlLogger, async db => { 207 | const r = await db.query('select 1 where 1 = 1'); 208 | if (r && JSON.stringify(r) == '[{"1":1}]') { 209 | response.statusCode = 204; 210 | } else { 211 | response.statusCode = 500; 212 | return { "error": "problem executing query", "query_result": r }; 213 | } 214 | }); 215 | } 216 | }); 217 | 218 | // Data-Set Features - Names must match files in the associated datasets directory. 219 | // If they exist then they are tracked for the purposes of this feature in SQLite. 220 | if(DATASETS) { 221 | server.get<{ Params: { template_name: DatasetTemplateName, }, Reply: DatasetGetTemplateResponse }>("/datasets/templates/:template_name", async (request, _response) => { 222 | server.log.info({ headers: request.headers, query: request.body, }, "datasets.templates.get"); 223 | const result = await getDataset(request.params.template_name); 224 | return result; 225 | }); 226 | 227 | // TODO: The name param here should be a DatasetCloneName, but this isn't being code-generated. 228 | server.post<{ Params: { clone_name: string, }, Body: DatasetCreateCloneRequest, Reply: DatasetCreateCloneResponse }>("/datasets/clones/:clone_name", async (request, _response) => { 229 | server.log.info({ headers: request.headers, query: request.body, }, "datasets.clones.post"); 230 | return cloneDataset(sqlLogger, request.params.clone_name, request.body); 231 | }); 232 | 233 | // TODO: The name param here should be a DatasetCloneName, but this isn't being code-generated. 234 | server.delete<{ Params: { clone_name: string, }, Reply: DatasetDeleteCloneResponse }>("/datasets/clones/:clone_name", async (request, _response) => { 235 | server.log.info({ headers: request.headers, query: request.body, }, "datasets.clones.delete"); 236 | return deleteDataset(request.params.clone_name); 237 | }); 238 | } 239 | 240 | server.get("/", async (request, response) => { 241 | response.type('text/html'); 242 | return ` 243 | 244 | 245 | Hasura Data Connectors SQLite Agent 246 | 247 | 248 |

Hasura Data Connectors SQLite Agent

249 |

See 250 | the GraphQL Engine repository for more information.

251 | 264 | 265 | 266 | `; 267 | }) 268 | 269 | process.on('SIGINT', () => { 270 | server.log.info("interrupted"); 271 | process.exit(0); 272 | }); 273 | 274 | const start = async () => { 275 | try { 276 | server.log.info(`STARTING on port ${port}`); 277 | await server.listen({port: port, host: "0.0.0.0"}); 278 | } 279 | catch (err) { 280 | server.log.fatal(err); 281 | process.exit(1); 282 | } 283 | }; 284 | start(); 285 | -------------------------------------------------------------------------------- /src/mutation.ts: -------------------------------------------------------------------------------- 1 | import { ArrayRelationInsertFieldValue, ColumnInsertFieldValue, DeleteMutationOperation, Expression, Field, InsertFieldSchema, InsertMutationOperation, MutationOperation, MutationOperationResults, MutationRequest, MutationResponse, ObjectRelationInsertFieldValue, Relationships, RowUpdate, TableInsertSchema, TableName, TableRelationships, UpdateMutationOperation } from "@hasura/dc-api-types"; 2 | import { Config } from "./config"; 3 | import { Connection, defaultMode, SqlLogger, withConnection } from "./db"; 4 | import { escapeIdentifier, escapeTableName, escapeTableNameSansSchema, json_object, where_clause, } from "./query"; 5 | import { asyncSequenceFromInputs, ErrorWithStatusCode, mapObjectToArray, tableNameEquals, tableToTarget, unreachable } from "./util"; 6 | 7 | // Types 8 | 9 | type Row = { 10 | ok: boolean, 11 | row: string, 12 | statement?: string, 13 | values?: Record, 14 | } 15 | 16 | type RowInfoValue = ColumnInsertFieldValue | ObjectRelationInsertFieldValue | ArrayRelationInsertFieldValue | null; 17 | 18 | type RowInfo = { 19 | field: string, 20 | schema: InsertFieldSchema, 21 | variable: string, 22 | value: RowInfoValue 23 | } 24 | 25 | type UpdateInfo = { 26 | variable: string, 27 | value: unknown, 28 | update: RowUpdate, 29 | } 30 | 31 | interface Info { 32 | variable: string, 33 | value: unknown 34 | } 35 | 36 | // Functions 37 | 38 | function escapeVariable(x: string): string { 39 | return x.replace(/[^a-zA-Z0-9]/g, ''); 40 | } 41 | 42 | function valuesString(infos: Array): string { 43 | return infos.map((info) => info.variable).join(', '); 44 | } 45 | 46 | function customUpdateOperator(operator: string, column: string, variable: string): string { 47 | switch(operator) { 48 | case 'inc': 49 | return `${column} + ${variable}`; 50 | case 'dec': 51 | return `${column} - ${variable}`; 52 | } 53 | throw Error(`Custom operator ${operator} is invalid. This should not happen.`); 54 | } 55 | 56 | function setString(infos: Array): string { 57 | return infos.map((info) => { 58 | const update = info.update; 59 | switch(update.type) { 60 | case 'custom_operator': 61 | return `${update.column} = ${customUpdateOperator(update.operator_name, update.column, info.variable)}` 62 | case 'set': 63 | return `${update.column} = ${info.variable}` 64 | default: 65 | return unreachable(update); 66 | } 67 | }).join(', '); 68 | } 69 | 70 | function columnsString(infos: Array): string { 71 | return infos.map((info) => { 72 | switch(info.schema.type) { 73 | case 'column': 74 | return escapeIdentifier(info.schema.column); 75 | default: 76 | throw(Error(`Type ${info.schema.type} for field ${info.field} is not currently supported.`)) 77 | } 78 | }).join(', '); 79 | } 80 | 81 | /** 82 | * @param schemas 83 | * @param table 84 | * @returns schema | null 85 | */ 86 | function getTableInsertSchema(schemas: Array, table: TableName): TableInsertSchema | null { 87 | for(var i = 0; i < schemas.length; i++) { 88 | const schema = schemas[i]; 89 | if(tableNameEquals(schema.table)(tableToTarget(table))) { 90 | return schema; 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | /** 97 | * 98 | * @param e 99 | * @returns boolean check on returned data 100 | * 101 | * Note: The heavy lifting is performed by `where_clause` from query.ts 102 | */ 103 | function whereString(relationships: Array, e: Expression, table: TableName): string { 104 | const w = where_clause(relationships, e, tableToTarget(table), escapeTableNameSansSchema(table)); 105 | return w; 106 | } 107 | 108 | /** 109 | * @param op 110 | * @returns SQLite expression that can be used in RETURNING clauses 111 | * 112 | * The `json_object` function from query.ts performs the heavy lifting here. 113 | */ 114 | function returningString(relationships: Array, fields: Record, table: TableName): string { 115 | /* Example of fields: 116 | { 117 | "ArtistId": { 118 | "type": "column", 119 | "column": "ArtistId", 120 | "column_type": "number" 121 | } 122 | } 123 | */ 124 | const r = json_object(relationships, fields, tableToTarget(table), escapeTableNameSansSchema(table)); 125 | return r; 126 | } 127 | 128 | function queryValues(info: Array): Record { 129 | return Object.fromEntries(info.map((x) => [x.variable, x.value])); 130 | } 131 | 132 | const EMPTY_AND: Expression = { type: 'and', expressions: [] }; 133 | 134 | function insertString(relationships: Array, op: InsertMutationOperation, info: Array): string { 135 | const columnValues = 136 | info.length > 0 137 | ? `(${columnsString(info)}) VALUES (${valuesString(info)})` 138 | : "DEFAULT VALUES"; 139 | 140 | return ` 141 | INSERT INTO ${escapeTableName(op.table)} ${columnValues} 142 | RETURNING 143 | ${returningString(relationships, op.returning_fields || {}, op.table)} as row, 144 | ${whereString(relationships, op.post_insert_check || EMPTY_AND, op.table)} as ok 145 | `; 146 | } 147 | 148 | function deleteString(relationships: Array, op: DeleteMutationOperation): string { 149 | return ` 150 | DELETE FROM ${escapeTableName(op.table)} 151 | WHERE ${whereString(relationships, op.where || EMPTY_AND, op.table)} 152 | RETURNING 153 | ${returningString(relationships, op.returning_fields || {}, op.table)} as row, 154 | 1=1 as ok 155 | `; 156 | } 157 | 158 | function updateString(relationships: Array, op: UpdateMutationOperation, info: Array): string { 159 | const result = ` 160 | UPDATE ${escapeTableName(op.table)} 161 | SET ${setString(info)} 162 | WHERE ${whereString(relationships, op.where || EMPTY_AND, op.table)} 163 | RETURNING 164 | ${returningString(relationships, op.returning_fields || {}, op.table)} as row, 165 | ${whereString(relationships, op.post_update_check || EMPTY_AND, op.table)} as ok 166 | `; 167 | return result; 168 | } 169 | 170 | /** 171 | * @param schemas 172 | * @param op 173 | * @returns Nested Array of RowInfo 174 | * 175 | * This function compiles all the useful information for constructing query-strings and variable data 176 | * into arrays of RowInfo packets. It is done this way to avoid repeated lookups and to keep the alignment 177 | * of identifiers, variables, and data in sync. 178 | */ 179 | function getInsertRowInfos(schemas: Array, op: InsertMutationOperation): Array { 180 | const schema = getTableInsertSchema(schemas, op.table); 181 | if(schema == null) { 182 | throw(Error(`Couldn't find insert schema for table ${escapeTableName(op.table)}`)); 183 | } 184 | return op.rows.map((row, rowIndex) => { 185 | const rowInfo = mapObjectToArray(row, ([fieldName,fieldValue], fieldIndex) => { 186 | const fieldSchema = schema.fields[fieldName]; 187 | if(fieldSchema == null) { 188 | throw(Error(`Couldn't find insert schema for field ${fieldName} for table ${escapeTableName(op.table)}`)); 189 | } 190 | return { 191 | field: fieldName, 192 | schema: fieldSchema, 193 | variable: `$${escapeVariable(fieldName)}_${rowIndex}_${fieldIndex}`, 194 | value: fieldValue 195 | }; 196 | }); 197 | return rowInfo; 198 | }); 199 | } 200 | 201 | function getUpdateRowInfos(op: UpdateMutationOperation): Array { 202 | return op.updates.map((update, i) => { 203 | return { 204 | variable: `$${escapeVariable(update.column)}_${i}`, 205 | value: update.value, 206 | update: update 207 | }; 208 | }); 209 | } 210 | 211 | async function insertRow(db: Connection, relationships: Array, op: InsertMutationOperation, info: Array): Promise> { 212 | const q = insertString(relationships, op, info); 213 | const v = queryValues(info); 214 | const results = await db.query(q,v); 215 | results.forEach((r) => { 216 | if(!r.ok) { 217 | r.statement = q 218 | r.values = v 219 | } 220 | }); 221 | return results; 222 | } 223 | 224 | async function updateRow(db: Connection, relationships: Array, op: UpdateMutationOperation, info: Array): Promise> { 225 | const q = updateString(relationships, op, info); 226 | const v = queryValues(info); 227 | const results = await db.query(q,v); 228 | results.forEach((r) => { 229 | if(!r.ok) { 230 | r.statement = q 231 | r.values = v 232 | } 233 | }); 234 | return results; 235 | } 236 | 237 | async function deleteRows(db: Connection, relationships: Array, op: DeleteMutationOperation): Promise> { 238 | const q = deleteString(relationships, op); 239 | const results = await db.query(q); 240 | return results; 241 | } 242 | 243 | function postMutationCheckError(op: MutationOperation, failed: Array): ErrorWithStatusCode { 244 | return ErrorWithStatusCode.mutationPermissionCheckFailure( 245 | "check constraint of an insert/update permission has failed", 246 | {op: op, results: failed} 247 | ); 248 | } 249 | 250 | async function mutationOperation(db: Connection, relationships: Array, schema: Array, op: MutationOperation): Promise { 251 | switch(op.type) { 252 | case 'insert': 253 | const infos = getInsertRowInfos(schema, op); 254 | await db.query('BEGIN',{}); 255 | // We split this operation into multiple inserts in case the inserted columns are hetrogenous: https://sqlite.org/forum/info/d7384e085b808b05 256 | const insertResultsSet = await asyncSequenceFromInputs(infos, (info) => insertRow(db, relationships, op, info)); 257 | const insertResults = ([] as Array).concat(...insertResultsSet); 258 | let insertFailed: Array = []; 259 | const mappedInsertResults = insertResults.map((row: Row) => { 260 | if (!row.ok) { 261 | insertFailed.push(row); 262 | } 263 | return JSON.parse(row.row); 264 | }); 265 | if(insertFailed.length > 0) { 266 | await db.query('ROLLBACK', {}); 267 | throw(postMutationCheckError(op, insertFailed)); 268 | } else { 269 | await db.query('COMMIT', {}); 270 | return { 271 | affected_rows: mappedInsertResults.length, 272 | ...(op.returning_fields ? { returning: mappedInsertResults } : {}) 273 | }; 274 | } 275 | 276 | case 'update': 277 | const updateInfo = getUpdateRowInfos(op); 278 | await db.query('BEGIN',{}); 279 | const resultSet = await updateRow(db, relationships, op, updateInfo); 280 | const updateResults = ([] as Array).concat(...resultSet); 281 | let updateFailed: Array = []; 282 | const mappedUpdateResults = updateResults.map((row: Row) => { 283 | if (!row.ok) { 284 | updateFailed.push(row); 285 | } 286 | return JSON.parse(row.row); 287 | }); 288 | if(updateFailed.length > 0) { 289 | await db.query('ROLLBACK', {}); 290 | throw(postMutationCheckError(op, updateFailed)); 291 | } else { 292 | await db.query('COMMIT', {}); 293 | return { 294 | affected_rows: mappedUpdateResults.length, 295 | ...(op.returning_fields ? { returning: mappedUpdateResults } : {}) 296 | }; 297 | } 298 | 299 | case 'delete': 300 | await db.query('BEGIN',{}); 301 | const deleteResults = await deleteRows(db, relationships, op); 302 | const mappedDeleteResults = deleteResults.map(row => JSON.parse(row.row)); 303 | await db.query('COMMIT',{}); 304 | return { 305 | affected_rows: mappedDeleteResults.length, 306 | ...(op.returning_fields ? { returning: mappedDeleteResults } : {}) 307 | }; 308 | 309 | default: 310 | return unreachable(op['type']); 311 | } 312 | } 313 | 314 | /** 315 | * @param config 316 | * @param sqlLogger 317 | * @param request 318 | * @returns MutationResponse 319 | * 320 | * Top-Level function for mutations. 321 | * This performs inserts/updates/deletes. 322 | */ 323 | export async function runMutation(config: Config, sqlLogger: SqlLogger, request: MutationRequest): Promise { 324 | return await withConnection(config, defaultMode, sqlLogger, async db => { 325 | const resultSet = await asyncSequenceFromInputs(request.operations, (op) => mutationOperation(db, request.relationships, request.insert_schema, op)); 326 | return { 327 | operation_results: resultSet 328 | }; 329 | }); 330 | } 331 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config"; 2 | import { defaultMode, SqlLogger, withConnection } from "./db"; 3 | import { coerceUndefinedToNull, tableNameEquals, unreachable, stringArrayEquals, ErrorWithStatusCode, mapObject } from "./util"; 4 | import { 5 | Expression, 6 | BinaryComparisonOperator, 7 | ComparisonValue, 8 | QueryRequest, 9 | ComparisonColumn, 10 | TableRelationships, 11 | Relationship, 12 | RelationshipField, 13 | BinaryArrayComparisonOperator, 14 | OrderBy, 15 | QueryResponse, 16 | Field, 17 | Aggregate, 18 | TableName, 19 | OrderDirection, 20 | UnaryComparisonOperator, 21 | ExplainResponse, 22 | ExistsExpression, 23 | OrderByRelation, 24 | OrderByElement, 25 | OrderByTarget, 26 | ScalarValue, 27 | InterpolatedRelationships, 28 | Target, 29 | InterpolatedQueries, 30 | Relationships, 31 | InterpolatedQuery, 32 | InterpolatedItem, 33 | } from "@hasura/dc-api-types"; 34 | import { customAlphabet } from "nanoid"; 35 | import { DEBUGGING_TAGS, QUERY_LENGTH_LIMIT } from "./environment"; 36 | 37 | const SqlString = require('sqlstring-sqlite'); 38 | 39 | const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789-_", 6); 40 | 41 | /** Helper type for convenience. Uses the sqlstring-sqlite library, but should ideally use the function in sequalize. 42 | */ 43 | type Fields = Record 44 | type Aggregates = Record 45 | 46 | function escapeString(x: any): string { 47 | return SqlString.escape(x); 48 | } 49 | 50 | /** 51 | * 52 | * @param identifier: Unescaped name. E.g. 'Alb"um' 53 | * @returns Escaped name. E.g. '"Alb\"um"' 54 | */ 55 | export function escapeIdentifier(identifier: string): string { 56 | // TODO: Review this function since the current implementation is off the cuff. 57 | const result = identifier.replace(/\\/g,"\\\\").replace(/"/g,'\\"'); 58 | return `"${result}"`; 59 | } 60 | 61 | /** 62 | * Throw an exception if the tableName has invalid number of prefix components. 63 | * 64 | * @param tableName: Unescaped table name. E.g. 'Alb"um' 65 | * @returns tableName 66 | */ 67 | function validateTableName(tableName: TableName): TableName { 68 | if (tableName.length <= 2 && tableName.length > 0) 69 | return tableName; 70 | else 71 | throw new Error(`${tableName.join(".")} is not a valid table`); 72 | } 73 | 74 | /** 75 | * @param ts 76 | * @returns last section of a qualified table array. E.g. [a,b] -> [b] 77 | */ 78 | export function getTableNameSansSchema(ts: Array): Array { 79 | return [ts[ts.length-1]]; 80 | } 81 | 82 | /** 83 | * 84 | * @param tableName: Unescaped table name. E.g. 'Alb"um' 85 | * @returns Escaped table name. E.g. '"Alb\"um"' 86 | */ 87 | export function escapeTableName(tableName: TableName): string { 88 | return validateTableName(tableName).map(escapeIdentifier).join("."); 89 | } 90 | 91 | export function escapeTargetName(target: Target): string { 92 | switch(target.type) { 93 | case 'table': 94 | return escapeTableName(target.name); 95 | case 'interpolated': 96 | return escapeTableName([target.id]); // Interpret as CTE reference 97 | default: 98 | throw(new ErrorWithStatusCode('`escapeTargetName` only implemented for tables and interpolated queries', 500, {target})); 99 | } 100 | } 101 | 102 | /** 103 | * @param tableName 104 | * @returns escaped tableName string with schema qualification removed 105 | * 106 | * This is useful in where clauses in returning statements where a qualified table name is invalid SQLite SQL. 107 | */ 108 | export function escapeTableNameSansSchema(tableName: TableName): string { 109 | return escapeTableName(getTableNameSansSchema(tableName)); 110 | } 111 | 112 | export function json_object(all_relationships: Relationships[], fields: Fields, target: Target, tableAlias: string): string { 113 | const result = Object.entries(fields).map(([fieldName, field]) => { 114 | switch(field.type) { 115 | case "column": 116 | return `${escapeString(fieldName)}, ${escapeIdentifier(field.column)}`; 117 | case "relationship": 118 | const relationships = find_relationships(all_relationships, target); 119 | const rel = relationships.relationships[field.relationship]; 120 | if(rel === undefined) { 121 | throw new Error(`Couldn't find relationship ${field.relationship} for field ${fieldName} on target ${JSON.stringify(target)}`); 122 | } 123 | return `'${fieldName}', ${relationship(all_relationships, rel, field, tableAlias)}`; 124 | case "object": 125 | throw new Error('Unsupported field type "object"'); 126 | case "array": 127 | throw new Error('Unsupported field type "array"'); 128 | default: 129 | return unreachable(field["type"]); 130 | } 131 | }).join(", "); 132 | 133 | return tag('json_object', `JSON_OBJECT(${result})`); 134 | } 135 | 136 | export function where_clause(relationships: Relationships[], expression: Expression, queryTarget: Target, queryTableAlias: string): string { 137 | const generateWhere = (expression: Expression, currentTarget: Target, currentTableAlias: string): string => { 138 | switch(expression.type) { 139 | case "not": 140 | const aNot = generateWhere(expression.expression, currentTarget, currentTableAlias); 141 | return `(NOT ${aNot})`; 142 | 143 | case "and": 144 | const aAnd = expression.expressions.flatMap(x => generateWhere(x, currentTarget, currentTableAlias)); 145 | return aAnd.length > 0 146 | ? `(${aAnd.join(" AND ")})` 147 | : "(1 = 1)" // true 148 | 149 | case "or": 150 | const aOr = expression.expressions.flatMap(x => generateWhere(x, currentTarget, currentTableAlias)); 151 | return aOr.length > 0 152 | ? `(${aOr.join(" OR ")})` 153 | : "(1 = 0)" // false 154 | 155 | case "exists": 156 | const joinInfo = calculateExistsJoinInfo(relationships, expression, currentTarget, currentTableAlias); 157 | const tableTarget: Target = joinInfo.joinTarget; 158 | const subqueryWhere = generateWhere(expression.where, tableTarget, joinInfo.joinTableAlias); 159 | const whereComparisons = [...joinInfo.joinComparisonFragments, subqueryWhere].join(" AND "); 160 | return tag('exists',`EXISTS (SELECT 1 FROM ${escapeTargetName(joinInfo.joinTarget)} AS ${joinInfo.joinTableAlias} WHERE ${whereComparisons})`); 161 | 162 | case "unary_op": 163 | const uop = uop_op(expression.operator); 164 | const columnFragment = generateComparisonColumnFragment(expression.column, queryTableAlias, currentTableAlias); 165 | return `(${columnFragment} ${uop})`; 166 | 167 | case "binary_op": 168 | const bopLhs = generateComparisonColumnFragment(expression.column, queryTableAlias, currentTableAlias); 169 | const bopRhs = generateComparisonValueFragment(expression.value, queryTableAlias, currentTableAlias); 170 | if(expression.operator == '_in_year') { 171 | return `cast(strftime('%Y', ${bopLhs}) as integer) = ${bopRhs}`; 172 | } else if(expression.operator == '_modulus_is_zero') { 173 | return `cast(${bopLhs} as integer) % ${bopRhs} = 0`; 174 | } else if(expression.operator == '_nand') { 175 | return `NOT (${bopLhs} AND ${bopRhs})`; 176 | } else if(expression.operator == '_nor') { 177 | return `NOT (${bopLhs} OR ${bopRhs})`; 178 | } else if(expression.operator == '_xor') { 179 | return `(${bopLhs} AND (NOT ${bopRhs})) OR ((NOT${bopRhs}) AND ${bopRhs})`; 180 | } else { 181 | const bop = bop_op(expression.operator); 182 | return `${bopLhs} ${bop} ${bopRhs}`; 183 | } 184 | 185 | case "binary_arr_op": 186 | const bopALhs = generateComparisonColumnFragment(expression.column, queryTableAlias, currentTableAlias); 187 | const bopA = bop_array(expression.operator); 188 | const bopARhsValues = expression.values.map(v => escapeString(v)).join(", "); 189 | return `(${bopALhs} ${bopA} (${bopARhsValues}))`; 190 | 191 | default: 192 | return unreachable(expression['type']); 193 | } 194 | }; 195 | 196 | return generateWhere(expression, queryTarget, queryTableAlias); 197 | } 198 | 199 | type ExistsJoinInfo = { 200 | joinTarget: Target, 201 | joinTableAlias: string, 202 | joinComparisonFragments: string[] 203 | } 204 | 205 | function calculateExistsJoinInfo(allRelationships: Relationships[], exists: ExistsExpression, sourceTarget: Target, sourceTableAlias: string): ExistsJoinInfo { 206 | switch (exists.in_table.type) { 207 | case "related": 208 | const tableRelationships = find_relationships(allRelationships, sourceTarget); 209 | const relationship = tableRelationships.relationships[exists.in_table.relationship]; 210 | const joinTableAlias = generateTargetAlias(relationship.target); 211 | 212 | const joinComparisonFragments = generateRelationshipJoinComparisonFragments(relationship, sourceTableAlias, joinTableAlias); 213 | 214 | return { 215 | joinTarget: relationship.target, 216 | joinTableAlias, 217 | joinComparisonFragments, 218 | }; 219 | 220 | case "unrelated": 221 | return { 222 | joinTarget: {type: 'table', name: exists.in_table.table}, 223 | joinTableAlias: generateTableAlias(exists.in_table.table), 224 | joinComparisonFragments: [] 225 | }; 226 | 227 | default: 228 | return unreachable(exists.in_table["type"]); 229 | } 230 | } 231 | 232 | function generateRelationshipJoinComparisonFragments(relationship: Relationship, sourceTableAlias: string, targetTableAlias: string): string[] { 233 | const sourceTablePrefix = `${sourceTableAlias}.`; 234 | return Object 235 | .entries(relationship.column_mapping) 236 | .map(([sourceColumnName, targetColumnName]) => 237 | `${sourceTablePrefix}${escapeIdentifier(sourceColumnName)} = ${targetTableAlias}.${escapeIdentifier(targetColumnName as string)}`); 238 | } 239 | 240 | function generateComparisonColumnFragment(comparisonColumn: ComparisonColumn, queryTableAlias: string, currentTableAlias: string): string { 241 | const path = comparisonColumn.path ?? []; 242 | const queryTablePrefix = queryTableAlias ? `${queryTableAlias}.` : ''; 243 | const currentTablePrefix = currentTableAlias ? `${currentTableAlias}.` : ''; 244 | const selector = getColumnSelector(comparisonColumn.name); 245 | if (path.length === 0) { 246 | return `${currentTablePrefix}${escapeIdentifier(selector)}` 247 | } else if (path.length === 1 && path[0] === "$") { 248 | return `${queryTablePrefix}${escapeIdentifier(selector)}` 249 | } else { 250 | throw new Error(`Unsupported path on ComparisonColumn: ${[...path, selector].join(".")}`); 251 | } 252 | } 253 | 254 | function generateComparisonValueFragment(comparisonValue: ComparisonValue, queryTableAlias: string, currentTableAlias: string): string { 255 | switch (comparisonValue.type) { 256 | case "column": 257 | return generateComparisonColumnFragment(comparisonValue.column, queryTableAlias, currentTableAlias); 258 | case "scalar": 259 | return escapeString(comparisonValue.value); 260 | default: 261 | return unreachable(comparisonValue["type"]); 262 | } 263 | } 264 | 265 | export function generateTargetAlias(target: Target): string { 266 | switch(target.type) { 267 | case 'function': 268 | throw new ErrorWithStatusCode("Can't create alias for functions", 500, {target}); 269 | case 'interpolated': 270 | return generateTableAlias([target.id]); 271 | case 'table': 272 | return generateTableAlias(target.name); 273 | } 274 | } 275 | 276 | function generateTableAlias(tableName: TableName): string { 277 | return generateIdentifierAlias(validateTableName(tableName).join("_")) 278 | } 279 | 280 | function generateIdentifierAlias(identifier: string): string { 281 | const randomSuffix = nanoid(); 282 | return escapeIdentifier(`${identifier}_${randomSuffix}`); 283 | } 284 | 285 | /** 286 | * 287 | * @param allTableRelationships Array of Table Relationships 288 | * @param tableName Table Name 289 | * @returns Relationships matching table-name 290 | */ 291 | function find_relationships(allRelationships: Relationships[], target: Target): Relationships { 292 | switch(target.type) { 293 | case 'table': 294 | for(var i = 0; i < allRelationships.length; i++) { 295 | const r = allRelationships[i] as TableRelationships; 296 | if(r.source_table != undefined && tableNameEquals(r.source_table)(target)) { 297 | return r; 298 | } 299 | } 300 | break 301 | case 'interpolated': 302 | for(var i = 0; i < allRelationships.length; i++) { 303 | const r = allRelationships[i] as InterpolatedRelationships; 304 | if(r.source_interpolated_query != undefined && r.source_interpolated_query == target.id) { 305 | return r; 306 | } 307 | } 308 | break; 309 | } 310 | throw new Error(`Couldn't find table relationships for target ${JSON.stringify(target)} - This shouldn't happen.`); 311 | } 312 | 313 | function cast_aggregate_function(f: string): string { 314 | switch(f) { 315 | case 'avg': 316 | case 'max': 317 | case 'min': 318 | case 'sum': 319 | case 'total': 320 | return f; 321 | default: 322 | throw new Error(`Aggregate function ${f} is not supported by SQLite. See: https://www.sqlite.org/lang_aggfunc.html`); 323 | } 324 | } 325 | 326 | /** 327 | * Builds an Aggregate query expression. 328 | */ 329 | function aggregates_query( 330 | allRelationships: Relationships[], 331 | target: Target, 332 | joinInfo: RelationshipJoinInfo | null, 333 | aggregates: Aggregates, 334 | wWhere: Expression | null, 335 | wLimit: number | null, 336 | wOffset: number | null, 337 | wOrder: OrderBy | null, 338 | ): string { 339 | 340 | const tableAlias = generateTargetAlias(target); 341 | const orderByInfo = orderBy(allRelationships, wOrder, target, tableAlias); 342 | const orderByJoinClauses = orderByInfo?.joinClauses.join(" ") ?? ""; 343 | const orderByClause = orderByInfo?.orderByClause ?? ""; 344 | const whereClause = where(allRelationships, wWhere, joinInfo, target, tableAlias); 345 | const sourceSubquery = `SELECT ${tableAlias}.* FROM ${escapeTargetName(target)} AS ${tableAlias} ${orderByJoinClauses} ${whereClause} ${orderByClause} ${limit(wLimit)} ${offset(wOffset)}` 346 | 347 | const aggregate_pairs = Object.entries(aggregates).map(([k,v]) => { 348 | switch(v.type) { 349 | case 'star_count': 350 | return `${escapeString(k)}, COUNT(*)`; 351 | case 'column_count': 352 | if(v.distinct) { 353 | return `${escapeString(k)}, COUNT(DISTINCT ${escapeIdentifier(v.column)})`; 354 | } else { 355 | return `${escapeString(k)}, COUNT(${escapeIdentifier(v.column)})`; 356 | } 357 | case 'single_column': 358 | return `${escapeString(k)}, ${cast_aggregate_function(v.function)}(${escapeIdentifier(v.column)})`; 359 | } 360 | }).join(', '); 361 | 362 | return `'aggregates', (SELECT JSON_OBJECT(${aggregate_pairs}) FROM (${sourceSubquery}))`; 363 | } 364 | 365 | type RelationshipJoinInfo = { 366 | sourceTableAlias: string 367 | columnMapping: Record // Mapping from source table column name to target table column name 368 | } 369 | 370 | function target_query( // TODO: Rename as `target_query` 371 | allRelationships: Relationships[], 372 | target: Target, 373 | joinInfo: RelationshipJoinInfo | null, 374 | fields: Fields | null, 375 | aggregates: Aggregates | null, 376 | wWhere: Expression | null, 377 | aggregatesLimit: number | null, 378 | wLimit: number | null, 379 | wOffset: number | null, 380 | wOrder: OrderBy | null, 381 | ): string { 382 | 383 | var tableName; 384 | 385 | // TableNames are resolved from IDs when using NQs. 386 | switch(target.type) { 387 | case 'table': tableName = target.name; break; 388 | case 'interpolated': tableName = [target.id]; break; 389 | case 'function': 390 | throw new ErrorWithStatusCode(`Can't execute table_query for UDFs`, 500, {target}); 391 | } 392 | 393 | const tableAlias = generateTargetAlias(target); 394 | const aggregateSelect = aggregates ? [aggregates_query(allRelationships, target, joinInfo, aggregates, wWhere, aggregatesLimit, wOffset, wOrder)] : []; 395 | // The use of the JSON function inside JSON_GROUP_ARRAY is necessary from SQLite 3.39.0 due to breaking changes in 396 | // SQLite. See https://sqlite.org/forum/forumpost/e3b101fb3234272b for more details. This approach still works fine 397 | // for older versions too. 398 | const fieldSelect = fields === null ? [] : [`'rows', JSON_GROUP_ARRAY(JSON(j))`]; 399 | const fieldFrom = fields === null ? '' : (() => { 400 | const whereClause = where(allRelationships, wWhere, joinInfo, target, tableAlias); 401 | // NOTE: The reuse of the 'j' identifier should be safe due to scoping. This is confirmed in testing. 402 | if(wOrder === null || wOrder.elements.length < 1) { 403 | return `FROM ( SELECT ${json_object(allRelationships, fields, target, tableAlias)} AS j FROM ${escapeTableName(tableName)} AS ${tableAlias} ${whereClause} ${limit(wLimit)} ${offset(wOffset)})`; 404 | } else { 405 | const orderByInfo = orderBy(allRelationships, wOrder, target, tableAlias); 406 | const orderByJoinClauses = orderByInfo?.joinClauses.join(" ") ?? ""; 407 | const orderByClause = orderByInfo?.orderByClause ?? ""; 408 | 409 | const innerSelect = `SELECT ${tableAlias}.* FROM ${escapeTableName(tableName)} AS ${tableAlias} ${orderByJoinClauses} ${whereClause} ${orderByClause} ${limit(wLimit)} ${offset(wOffset)}`; 410 | 411 | const wrappedQueryTableAlias = generateTableAlias(tableName); 412 | return `FROM (SELECT ${json_object(allRelationships, fields, target, wrappedQueryTableAlias)} AS j FROM (${innerSelect}) AS ${wrappedQueryTableAlias})`; 413 | } 414 | })() 415 | 416 | return tag('table_query',`(SELECT JSON_OBJECT(${[...fieldSelect, ...aggregateSelect].join(', ')}) ${fieldFrom})`); 417 | } 418 | 419 | function relationship(ts: Relationships[], r: Relationship, field: RelationshipField, sourceTableAlias: string): string { 420 | const relationshipJoinInfo = { 421 | sourceTableAlias, 422 | columnMapping: r.column_mapping as Record, 423 | }; 424 | 425 | // We force a limit of 1 for object relationships in case the user has configured a manual 426 | // "object" relationship that accidentally actually is an array relationship 427 | const [limit, aggregatesLimit] = 428 | r.relationship_type === "object" 429 | ? [1, 1] 430 | : [coerceUndefinedToNull(field.query.limit), coerceUndefinedToNull(field.query.aggregates_limit)]; 431 | 432 | return tag("relationship", target_query( 433 | ts, 434 | r.target, 435 | relationshipJoinInfo, 436 | coerceUndefinedToNull(field.query.fields), 437 | coerceUndefinedToNull(field.query.aggregates), 438 | coerceUndefinedToNull(field.query.where), 439 | aggregatesLimit, 440 | limit, 441 | coerceUndefinedToNull(field.query.offset), 442 | coerceUndefinedToNull(field.query.order_by), 443 | )); 444 | } 445 | 446 | function bop_array(o: BinaryArrayComparisonOperator): string { 447 | switch(o) { 448 | case 'in': return tag('bop_array','IN'); 449 | default: return tag('bop_array', o); 450 | } 451 | } 452 | 453 | function bop_op(o: BinaryComparisonOperator): string { 454 | let result = o; 455 | switch(o) { 456 | // TODO: Check for coverage of these operators 457 | case 'equal': result = '='; break; 458 | case 'greater_than': result = '>'; break; 459 | case 'greater_than_or_equal': result = '>='; break; 460 | case 'less_than': result = '<'; break; 461 | case 'less_than_or_equal': result = '<='; break; 462 | case '_like': result = 'LIKE'; break; 463 | case '_glob': result = 'GLOB'; break; 464 | case '_regexp': result = 'REGEXP'; break; // TODO: Have capabilities detect if REGEXP support is enabled 465 | case '_and': result = 'AND'; break; 466 | case '_or': result = 'OR'; break; 467 | } 468 | // TODO: We can't always assume that we can include the operator here verbatim. 469 | return tag('bop_op',result); 470 | } 471 | 472 | function uop_op(o: UnaryComparisonOperator): string { 473 | let result = o; 474 | switch(o) { 475 | case 'is_null': result = "IS NULL"; break; 476 | } 477 | return tag('uop_op',result); 478 | } 479 | 480 | function orderDirection(orderDirection: OrderDirection): string { 481 | switch (orderDirection) { 482 | case "asc": return "ASC NULLS LAST"; 483 | case "desc": return "DESC NULLS FIRST"; 484 | default: 485 | return unreachable(orderDirection); 486 | } 487 | } 488 | 489 | type OrderByInfo = { 490 | joinClauses: string[], 491 | orderByClause: string, 492 | } 493 | 494 | function orderBy(allRelationships: Relationships[], orderBy: OrderBy | null, queryTarget: Target, queryTableAlias: string): OrderByInfo | null { 495 | if (orderBy === null || orderBy.elements.length < 1) { 496 | return null; 497 | } 498 | 499 | const joinInfos = Object 500 | .entries(orderBy.relations) 501 | .flatMap(([subrelationshipName, subrelation]) => 502 | generateOrderByJoinClause(allRelationships, orderBy.elements, [], subrelationshipName, subrelation, queryTarget, queryTableAlias) 503 | ); 504 | 505 | const orderByFragments = 506 | orderBy.elements 507 | .map(orderByElement => { 508 | const targetTableAlias = orderByElement.target_path.length === 0 509 | ? queryTableAlias 510 | : (() => { 511 | const joinInfo = joinInfos.find(joinInfo => joinInfo.joinTableType === getJoinTableTypeForTarget(orderByElement.target) && stringArrayEquals(joinInfo.relationshipPath)(orderByElement.target_path)); 512 | if (joinInfo === undefined) throw new Error("Can't find a join table for order by target."); // Should not happen 😉 513 | return joinInfo.tableAlias; 514 | })(); 515 | 516 | const targetColumn = `${targetTableAlias}.${getOrderByTargetAlias(orderByElement.target)}` 517 | 518 | const targetExpression = orderByElement.target.type === "star_count_aggregate" 519 | ? `COALESCE(${targetColumn}, 0)` 520 | : targetColumn 521 | 522 | return `${targetExpression} ${orderDirection(orderByElement.order_direction)}`; 523 | }); 524 | 525 | return { 526 | joinClauses: joinInfos.map(joinInfo => joinInfo.joinClause), 527 | orderByClause: tag('orderBy',`ORDER BY ${orderByFragments.join(",")}`), 528 | }; 529 | } 530 | 531 | type OrderByJoinTableType = "column" | "aggregate"; 532 | 533 | function getJoinTableTypeForTarget(orderByTarget: OrderByTarget): OrderByJoinTableType { 534 | switch (orderByTarget.type) { 535 | case "column": return "column"; 536 | case "star_count_aggregate": return "aggregate"; 537 | case "single_column_aggregate": return "aggregate"; 538 | default: 539 | return unreachable(orderByTarget["type"]); 540 | } 541 | } 542 | 543 | type OrderByJoinInfo = { 544 | joinTableType: OrderByJoinTableType, 545 | relationshipPath: string[], 546 | tableAlias: string, 547 | joinClause: string, 548 | } 549 | 550 | function generateOrderByJoinClause( 551 | allRelationships: Relationships[], 552 | allOrderByElements: OrderByElement[], 553 | parentRelationshipNames: string[], 554 | relationshipName: string, 555 | orderByRelation: OrderByRelation, 556 | sourceTarget: Target, 557 | sourceTableAlias: string 558 | ): OrderByJoinInfo[] { 559 | const relationshipPath = [...parentRelationshipNames, relationshipName]; 560 | const relationships = find_relationships(allRelationships, sourceTarget); 561 | const relationship = relationships.relationships[relationshipName]; 562 | 563 | const orderByElements = allOrderByElements.filter(byTargetPath(relationshipPath)); 564 | const columnTargetsExist = orderByElements.some(element => getJoinTableTypeForTarget(element.target) === "column"); 565 | const aggregateElements = orderByElements.filter(element => getJoinTableTypeForTarget(element.target) === "aggregate"); 566 | 567 | const [columnTargetJoin, subrelationJoinInfo] = (() => { 568 | const subrelationsExist = Object.keys(orderByRelation.subrelations).length > 0; 569 | if (columnTargetsExist || subrelationsExist) { 570 | const columnTargetJoin = generateOrderByColumnTargetJoinInfo(allRelationships, relationshipPath, relationship, sourceTableAlias, orderByRelation.where); 571 | 572 | const subrelationJoinInfo = Object 573 | .entries(orderByRelation.subrelations) 574 | .flatMap(([subrelationshipName, subrelation]) => 575 | generateOrderByJoinClause(allRelationships, allOrderByElements, relationshipPath, subrelationshipName, subrelation, relationship.target, columnTargetJoin.tableAlias) 576 | ); 577 | 578 | return [[columnTargetJoin], subrelationJoinInfo] 579 | 580 | } else { 581 | return [[], []]; 582 | } 583 | })(); 584 | 585 | const aggregateTargetJoin = aggregateElements.length > 0 586 | ? [generateOrderByAggregateTargetJoinInfo(allRelationships, relationshipPath, relationship, sourceTableAlias, orderByRelation.where, aggregateElements)] 587 | : []; 588 | 589 | 590 | return [ 591 | ...columnTargetJoin, 592 | ...aggregateTargetJoin, 593 | ...subrelationJoinInfo 594 | ]; 595 | } 596 | 597 | const byTargetPath = (relationshipPath: string[]) => (orderByElement: OrderByElement): boolean => stringArrayEquals(orderByElement.target_path)(relationshipPath); 598 | 599 | function generateOrderByColumnTargetJoinInfo( 600 | allRelationships: Relationships[], 601 | relationshipPath: string[], 602 | relationship: Relationship, 603 | sourceTableAlias: string, 604 | whereExpression: Expression | undefined 605 | ): OrderByJoinInfo { 606 | const targetTableAlias = generateTargetAlias(relationship.target); 607 | 608 | const joinComparisonFragments = generateRelationshipJoinComparisonFragments(relationship, sourceTableAlias, targetTableAlias); 609 | const whereComparisons = whereExpression ? [where_clause(allRelationships, whereExpression, relationship.target, targetTableAlias)] : []; 610 | const joinOnFragment = [...joinComparisonFragments, ...whereComparisons].join(" AND "); 611 | 612 | const joinClause = tag("columnTargetJoin", `LEFT JOIN ${escapeTargetName(relationship.target)} AS ${targetTableAlias} ON ${joinOnFragment}`); 613 | return { 614 | joinTableType: "column", 615 | relationshipPath: relationshipPath, 616 | tableAlias: targetTableAlias, 617 | joinClause: joinClause 618 | }; 619 | } 620 | 621 | function generateOrderByAggregateTargetJoinInfo( 622 | allTableRelationships: Relationships[], 623 | relationshipPath: string[], 624 | relationship: Relationship, 625 | sourceTableAlias: string, 626 | whereExpression: Expression | undefined, 627 | aggregateElements: OrderByElement[], 628 | ): OrderByJoinInfo { 629 | 630 | const targetTableAlias = generateTargetAlias(relationship.target); 631 | const subqueryTableAlias = generateTargetAlias(relationship.target); 632 | 633 | const aggregateColumnsFragments = aggregateElements.flatMap(element => { 634 | switch (element.target.type) { 635 | case "column": return []; 636 | case "star_count_aggregate": return `COUNT(*) AS ${getOrderByTargetAlias(element.target)}`; 637 | case "single_column_aggregate": return `${cast_aggregate_function(element.target.function)}(${escapeIdentifier(element.target.column)}) AS ${getOrderByTargetAlias(element.target)}`; 638 | default: unreachable(element.target["type"]); 639 | } 640 | }); 641 | const joinColumns = Object.values(relationship.column_mapping as Record).map(escapeIdentifier); 642 | const selectColumns = [...joinColumns, aggregateColumnsFragments]; 643 | const whereClause = whereExpression ? `WHERE ${where_clause(allTableRelationships, whereExpression, relationship.target, subqueryTableAlias)}` : ""; 644 | const aggregateSubquery = `SELECT ${selectColumns.join(", ")} FROM ${escapeTargetName(relationship.target)} AS ${subqueryTableAlias} ${whereClause} GROUP BY ${joinColumns.join(", ")}` 645 | 646 | const joinComparisonFragments = generateRelationshipJoinComparisonFragments(relationship, sourceTableAlias, targetTableAlias); 647 | const joinOnFragment = [ ...joinComparisonFragments ].join(" AND "); 648 | const joinClause = tag("aggregateTargetJoin", `LEFT JOIN (${aggregateSubquery}) AS ${targetTableAlias} ON ${joinOnFragment}`) 649 | return { 650 | joinTableType: "aggregate", 651 | relationshipPath: relationshipPath, 652 | tableAlias: targetTableAlias, 653 | joinClause: joinClause 654 | }; 655 | } 656 | 657 | function getOrderByTargetAlias(orderByTarget: OrderByTarget): string { 658 | switch (orderByTarget.type) { 659 | case "column": return escapeIdentifier(getColumnSelector(orderByTarget.column)); 660 | case "star_count_aggregate": return escapeIdentifier("__star_count__"); 661 | case "single_column_aggregate": return escapeIdentifier(`__${orderByTarget.function}_${orderByTarget.column}__`); 662 | default: 663 | return unreachable(orderByTarget["type"]); 664 | } 665 | } 666 | 667 | /** 668 | * @param whereExpression Nested expression used in the associated where clause 669 | * @param joinInfo Information about a possible join from a source table to the query table that needs to be generated into the where clause 670 | * @returns string representing the combined where clause 671 | */ 672 | function where(allRelationships: Relationships[], whereExpression: Expression | null, joinInfo: RelationshipJoinInfo | null, queryTarget: Target, queryTableAlias: string): string { 673 | const whereClause = whereExpression !== null ? [where_clause(allRelationships, whereExpression, queryTarget, queryTableAlias)] : []; 674 | const joinArray = joinInfo 675 | ? Object 676 | .entries(joinInfo.columnMapping) 677 | .map(([sourceColumn, targetColumn]) => 678 | `${joinInfo.sourceTableAlias}.${escapeIdentifier(sourceColumn)} = ${queryTableAlias}.${escapeIdentifier(targetColumn)}` 679 | ) 680 | : [] 681 | 682 | const clauses = [...whereClause, ...joinArray]; 683 | return clauses.length < 1 684 | ? "" 685 | : tag('where',`WHERE ${clauses.join(" AND ")}`); 686 | } 687 | 688 | function limit(l: number | null): string { 689 | if(l === null) { 690 | return ""; 691 | } else { 692 | return tag('limit',`LIMIT ${l}`); 693 | } 694 | } 695 | 696 | function offset(o: number | null): string { 697 | if(o === null) { 698 | return ""; 699 | } else { 700 | return tag('offset', `OFFSET ${o}`); 701 | } 702 | } 703 | 704 | // NOTE: InterpolatedItem can be extended to support arrays of values. 705 | // NOTE: It appears that value.value_type comes back as `Boolean` when we advertise only items from `ScalarTypeKey` 706 | function cte_item(value: InterpolatedItem): string { 707 | switch(value.type) { 708 | case 'text': 709 | return value.value; 710 | case 'scalar': 711 | switch(value.value_type.toLowerCase()) { 712 | // Check this list against the types listed in capabilities 713 | case 'string': 714 | return escapeString(value.value); 715 | case 'number': 716 | case 'int': 717 | case 'integer': 718 | case 'float': 719 | return `${value.value}`; 720 | case 'bool': 721 | case 'boolean': 722 | return `${value.value ? 1 : 0}`; 723 | // Assume that everything else is a JSON value 724 | case 'json': 725 | default: 726 | return `json( ${ escapeString(JSON.stringify(value.value)) } )`; 727 | } 728 | default: 729 | return unreachable(value["type"]); 730 | } 731 | } 732 | 733 | function cte_items(iq: InterpolatedQuery): string { 734 | const items = iq.items.map(cte_item); 735 | const joined = items.join(' '); 736 | return `${iq.id} AS ( ${joined} )`; 737 | } 738 | 739 | function cte_block(qs: InterpolatedQueries): string { 740 | const ctes = Object.entries(qs).map(([_id, iq], _ix) => cte_items(iq)); 741 | const cte_string = ctes.join(', '); 742 | return `WITH ${cte_string}`; 743 | } 744 | 745 | function query(request: QueryRequest): string { 746 | const cte = request.interpolated_queries == null ? '' : cte_block(request.interpolated_queries); 747 | const result = target_query( 748 | request.relationships, 749 | request.target, 750 | null, 751 | coerceUndefinedToNull(request.query.fields), 752 | coerceUndefinedToNull(request.query.aggregates), 753 | coerceUndefinedToNull(request.query.where), 754 | coerceUndefinedToNull(request.query.aggregates_limit), 755 | coerceUndefinedToNull(request.query.limit), 756 | coerceUndefinedToNull(request.query.offset), 757 | coerceUndefinedToNull(request.query.order_by), 758 | ); 759 | return tag('query', `${cte} SELECT ${result} as data`); 760 | } 761 | 762 | /** 763 | * Creates a SELECT statement that returns rows for the foreach ids. 764 | * 765 | * Given: 766 | * ``` 767 | * [ 768 | * {"columnA": {"value": "A1", "value_type": "string" }, "columnB": {"value": B1, "value_type": "string" }}, 769 | * {"columnA": {"value": "A2", "value_type": "string" }, "columnB": {"value": B2, "value_type": "string" }} 770 | * ] 771 | * ``` 772 | * 773 | * We will generate the following SQL: 774 | * 775 | * ``` 776 | * SELECT value ->> '$.columnA' AS "columnA", value ->> '$.columnB' AS "columnB" 777 | * FROM JSON_EACH('[{"columnA":"A1","columnB":"B1"},{"columnA":"A2","columnB":"B2"}]') 778 | * ``` 779 | */ 780 | function foreach_ids_table_value(foreachIds: Record[]): string { 781 | const columnNames = Object.keys(foreachIds[0]); 782 | 783 | const columns = columnNames.map(name => `value ->> ${escapeString("$." + name)} AS ${escapeIdentifier(name)}`); 784 | const jsonData = foreachIds.map(ids => mapObject(ids, ([column, scalarValue]) => [column, scalarValue.value])); 785 | 786 | return tag('foreach_ids_table_value', `SELECT ${columns} FROM JSON_EACH(${escapeString(JSON.stringify(jsonData))})`) 787 | } 788 | 789 | /** 790 | * Creates SQL query for a foreach query request. 791 | * 792 | * This is done by creating a CTE table that contains the foreach ids, and then wrapping 793 | * the existing query in a new one that joins from the CTE virtual table to the original query table 794 | * using a generated table relationship and fields list. 795 | * 796 | * The SQL we generate looks like this: 797 | * 798 | *``` 799 | * WITH foreach_ids_xxx AS ( 800 | * SELECT ... FROM ... (see foreach_ids_table_value) 801 | * ) 802 | * SELECT table_subquery AS data 803 | * ``` 804 | */ 805 | function foreach_query(foreachIds: Record[], request: QueryRequest): string { 806 | 807 | const randomSuffix = nanoid(); 808 | const foreachTableName: TableName = [`foreach_ids_${randomSuffix}`]; 809 | const foreachRelationshipName = "Foreach"; 810 | const foreachTableRelationship: TableRelationships = { 811 | type: 'table', 812 | source_table: foreachTableName, 813 | relationships: { 814 | [foreachRelationshipName]: { 815 | relationship_type: "array", 816 | target: request.target, 817 | column_mapping: mapObject(foreachIds[0], ([columnName, _scalarValue]) => [columnName, columnName]) 818 | } 819 | } 820 | }; 821 | const foreachQueryFields: Record = { 822 | "query": { 823 | type: "relationship", 824 | relationship: foreachRelationshipName, 825 | query: request.query 826 | } 827 | }; 828 | 829 | const foreachIdsTableValue = foreach_ids_table_value(foreachIds); 830 | const tableSubquery = target_query( 831 | [foreachTableRelationship, ...(request.relationships)], 832 | {type: 'table', name: foreachTableName}, // Note: expand to other target types 833 | null, 834 | foreachQueryFields, 835 | null, 836 | null, 837 | null, 838 | null, 839 | null, 840 | null, 841 | ); 842 | return tag('foreach_query', `WITH ${escapeTableName(foreachTableName)} AS (${foreachIdsTableValue}) SELECT ${tableSubquery} AS data`); 843 | } 844 | 845 | /** Function to add SQL comments to the generated SQL to tag which procedures generated what text. 846 | * 847 | * comment('a','b') => '/*\\*\/ b /*\*\/' 848 | */ 849 | function tag(t: string, s: string): string { 850 | if(DEBUGGING_TAGS) { 851 | return `/*<${t}>*/ ${s} /**/`; 852 | } else { 853 | return s; 854 | } 855 | } 856 | 857 | /** Performs a query and returns results 858 | * 859 | * Limitations: 860 | * 861 | * - Binary Array Operations not currently supported. 862 | * 863 | * The current algorithm is to first create a query, then execute it, returning results. 864 | * 865 | * Method for adding relationship fields: 866 | * 867 | * - JSON aggregation similar to Postgres' approach. 868 | * - 4.13. The json_group_array() and json_group_object() aggregate SQL functions 869 | * - https://www.sqlite.org/json1.html#jgrouparray 870 | * 871 | 872 | 873 | * Example of a test query: 874 | * 875 | * ``` 876 | * query MyQuery { 877 | * Artist(limit: 5, order_by: {ArtistId: asc}, where: {Name: {_neq: "Accept"}, _and: {Name: {_is_null: false}}}, offset: 3) { 878 | * ArtistId 879 | * Name 880 | * Albums(where: {Title: {_is_null: false, _gt: "A", _nin: "foo"}}, limit: 2) { 881 | * AlbumId 882 | * Title 883 | * ArtistId 884 | * Tracks(limit: 1) { 885 | * Name 886 | * TrackId 887 | * } 888 | * Artist { 889 | * ArtistId 890 | * } 891 | * } 892 | * } 893 | * Track(limit: 3) { 894 | * Name 895 | * Album { 896 | * Title 897 | * } 898 | * } 899 | * } 900 | * ``` 901 | * 902 | */ 903 | export async function queryData(config: Config, sqlLogger: SqlLogger, request: QueryRequest): Promise { 904 | return await withConnection(config, defaultMode, sqlLogger, async db => { 905 | const q = 906 | request.foreach 907 | ? foreach_query(request.foreach, request) 908 | : query(request); 909 | 910 | if(q.length > QUERY_LENGTH_LIMIT) { 911 | const error = new ErrorWithStatusCode( 912 | `Generated SQL Query was too long (${q.length} > ${QUERY_LENGTH_LIMIT})`, 913 | 500, 914 | { "query.length": q.length, "limit": QUERY_LENGTH_LIMIT } 915 | ); 916 | throw error; 917 | } 918 | 919 | const results = await db.query(q); 920 | return JSON.parse(results[0].data); 921 | }); 922 | } 923 | 924 | /** 925 | * 926 | * Constructs a query as per the `POST /query` endpoint but prefixes it with `EXPLAIN QUERY PLAN` before execution. 927 | * 928 | * Formatted result lines are included under the `lines` field. An initial blank line is included to work around a display bug. 929 | * 930 | * NOTE: The Explain related items are included here since they are a small extension of Queries, and another module may be overkill. 931 | * 932 | * @param config 933 | * @param sqlLogger 934 | * @param queryRequest 935 | * @returns 936 | */ 937 | export async function explain(config: Config, sqlLogger: SqlLogger, request: QueryRequest): Promise { 938 | return await withConnection(config, defaultMode, sqlLogger, async db => { 939 | const q = query(request); 940 | const result = await db.query(`EXPLAIN QUERY PLAN ${q}`); 941 | return { 942 | query: q, 943 | lines: [ "", ...formatExplainLines(result as AnalysisEntry[])] 944 | }; 945 | }); 946 | } 947 | 948 | function formatExplainLines(items: AnalysisEntry[]): string[] { 949 | const lines = Object.fromEntries(items.map(x => [x.id, x])); 950 | function depth(x: number): number { 951 | if(x < 1) { 952 | return 0; 953 | } 954 | return 2 + depth(lines[x].parent); 955 | } 956 | return items.map(x => `${' '.repeat(depth(x.parent))}${x.detail}`) 957 | } 958 | 959 | type AnalysisEntry = { 960 | id: number, 961 | parent: number, 962 | detail: string 963 | } 964 | 965 | const getColumnSelector = (columnSelector: string | Array): string => { 966 | if (typeof columnSelector === "string") 967 | return columnSelector; 968 | return columnSelector[0]; 969 | } 970 | -------------------------------------------------------------------------------- /src/raw.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config"; 2 | import { withConnection, SqlLogger, defaultMode } from './db'; 3 | import { RawRequest, RawResponse } from '@hasura/dc-api-types'; 4 | 5 | export async function runRawOperation(config: Config, sqlLogger: SqlLogger, query: RawRequest): Promise { 6 | return await withConnection(config, defaultMode, sqlLogger, async db => { 7 | const results = await db.query(query.query); 8 | 9 | return { 10 | rows: (results || []) as Record[] 11 | }; 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | import { SchemaResponse, ColumnInfo, TableInfo, Constraint, ColumnValueGenerationStrategy, SchemaRequest, DetailLevel, TableName } from "@hasura/dc-api-types" 2 | import { ScalarTypeKey } from "./capabilities"; 3 | import { Config } from "./config"; 4 | import { defaultMode, SqlLogger, withConnection } from './db'; 5 | import { MUTATIONS } from "./environment"; 6 | import { unreachable } from "./util"; 7 | 8 | var sqliteParser = require('sqlite-parser'); 9 | 10 | type TableInfoInternal = { 11 | name: string, 12 | type: string, 13 | tbl_name: string, 14 | rootpage: Number, 15 | sql: string 16 | } 17 | 18 | type Datatype = { 19 | affinity: string, // Sqlite affinity, lowercased 20 | variant: string, // Declared type, lowercased 21 | } 22 | 23 | // Note: Using ScalarTypeKey here instead of ScalarType to show that we have only used 24 | // the capability documented types, and that ScalarTypeKey is a subset of ScalarType 25 | function determineScalarType(datatype: Datatype): ScalarTypeKey { 26 | switch (datatype.variant) { 27 | case "bool": return "bool"; 28 | case "boolean": return "bool"; 29 | case "datetime": return "DateTime"; 30 | } 31 | switch (datatype.affinity) { 32 | case "integer": return "number"; 33 | case "real": return "number"; 34 | case "numeric": return "number"; 35 | case "text": return "string"; 36 | default: 37 | console.log(`Unknown SQLite column type: ${datatype.variant} (affinity: ${datatype.affinity}). Interpreting as string.`); 38 | return "string"; 39 | } 40 | } 41 | 42 | function getColumns(ast: any[]) : ColumnInfo[] { 43 | return ast.map(column => { 44 | const isAutoIncrement = column.definition.some((def: any) => def.type === "constraint" && def.autoIncrement === true); 45 | 46 | return { 47 | name: column.name, 48 | type: determineScalarType(column.datatype), 49 | nullable: nullableCast(column.definition), 50 | insertable: MUTATIONS, 51 | updatable: MUTATIONS, 52 | ...(isAutoIncrement ? { value_generated: { type: "auto_increment" } } : {}) 53 | }; 54 | }) 55 | } 56 | 57 | function nullableCast(ds: any[]): boolean { 58 | for(var d of ds) { 59 | if(d.type === 'constraint' && d.variant == 'not null') { 60 | return false; 61 | } 62 | } 63 | return true; 64 | } 65 | 66 | const formatTableInfo = (config: Config, detailLevel: DetailLevel): ((info: TableInfoInternal) => TableInfo) => { 67 | switch (detailLevel) { 68 | case "everything": return formatEverythingTableInfo(config); 69 | case "basic_info": return formatBasicTableInfo(config); 70 | default: return unreachable(detailLevel); 71 | } 72 | } 73 | 74 | const formatBasicTableInfo = (config: Config) => (info: TableInfoInternal): TableInfo => { 75 | const tableName = config.explicit_main_schema ? ["main", info.name] : [info.name]; 76 | return { 77 | name: tableName, 78 | type: "table" 79 | } 80 | } 81 | 82 | const formatEverythingTableInfo = (config: Config) => (info: TableInfoInternal): TableInfo => { 83 | const basicTableInfo = formatBasicTableInfo(config)(info); 84 | const ast = sqliteParser(info.sql); 85 | const columnsDdl = getColumnsDdl(ast); 86 | const primaryKeys = getPrimaryKeyNames(ast); 87 | const foreignKeys = ddlFKs(config, basicTableInfo.name, ast); 88 | const primaryKey = primaryKeys.length > 0 ? { primary_key: primaryKeys } : {}; 89 | const foreignKey = foreignKeys.length > 0 ? { foreign_keys: Object.fromEntries(foreignKeys) } : {}; 90 | 91 | return { 92 | ...basicTableInfo, 93 | ...primaryKey, 94 | ...foreignKey, 95 | description: info.sql, 96 | columns: getColumns(columnsDdl), 97 | insertable: MUTATIONS, 98 | updatable: MUTATIONS, 99 | deletable: MUTATIONS, 100 | } 101 | } 102 | 103 | /** 104 | * @param table 105 | * @returns true if the table is an SQLite meta table such as a sequence, index, etc. 106 | */ 107 | function isMeta(table : TableInfoInternal) { 108 | return table.type != 'table' || table.name === 'sqlite_sequence'; 109 | } 110 | 111 | const includeTable = (config: Config, only_tables?: TableName[]) => (table: TableInfoInternal): boolean => { 112 | if (isMeta(table) && !config.meta) { 113 | return false; 114 | } 115 | 116 | const filterForOnlyTheseTables = only_tables 117 | // If we're using an explicit main schema, only use those table names that belong to that schema 118 | ?.filter(n => config.explicit_main_schema ? n.length === 2 && n[0] === "main" : true) 119 | // Just keep the actual table name 120 | ?.map(n => n[n.length - 1]) 121 | 122 | if (config.tables || only_tables) { 123 | return (config.tables ?? []).concat(filterForOnlyTheseTables ?? []).indexOf(table.name) >= 0; 124 | } else { 125 | return true; 126 | } 127 | } 128 | 129 | /** 130 | * Pulls columns from the output of sqlite-parser. 131 | * Note that this doesn't check if duplicates are present and will emit them as many times as they are present. 132 | * This is done as an easy way to preserve order. 133 | * 134 | * @param ddl - The output of sqlite-parser 135 | * @returns - List of columns as present in the output of sqlite-parser. 136 | */ 137 | function getColumnsDdl(ddl: any): any[] { 138 | if(ddl.type != 'statement' || ddl.variant != 'list') { 139 | throw new Error("Encountered a non-statement or non-list when parsing DDL for table."); 140 | } 141 | return ddl.statement.flatMap((t: any) => { 142 | if(t.type != 'statement' || t.variant != 'create' || t.format != 'table') { 143 | return []; 144 | } 145 | return t.definition.flatMap((c: any) => { 146 | if(c.type != 'definition' || c.variant != 'column') { 147 | return []; 148 | } 149 | return [c]; 150 | }); 151 | }) 152 | } 153 | 154 | /** 155 | * Example: 156 | * 157 | * foreign_keys: { 158 | * "ArtistId->Artist.ArtistId": { 159 | * column_mapping: { 160 | * "ArtistId": "ArtistId" 161 | * }, 162 | * foreign_table: "Artist", 163 | * } 164 | * } 165 | * 166 | * NOTE: We currently don't log if the structure of the DDL is unexpected, which could be the case for composite FKs, etc. 167 | * NOTE: There could be multiple paths between tables. 168 | * NOTE: Composite keys are not currently supported. 169 | * 170 | * @param ddl 171 | * @returns [name, FK constraint definition][] 172 | */ 173 | function ddlFKs(config: Config, tableName: Array, ddl: any): [string, Constraint][] { 174 | if(ddl.type != 'statement' || ddl.variant != 'list') { 175 | throw new Error("Encountered a non-statement or non-list DDL for table."); 176 | } 177 | return ddl.statement.flatMap((t: any) => { 178 | if(t.type != 'statement' || t.variant != 'create' || t.format != 'table') { 179 | return []; 180 | } 181 | return t.definition.flatMap((c: any) => { 182 | if(c.type != 'definition' || c.variant != 'constraint' 183 | || c.definition.length != 1 || c.definition[0].type != 'constraint' || c.definition[0].variant != 'foreign key') { 184 | return []; 185 | } 186 | if(c.columns.length != 1) { 187 | return []; 188 | } 189 | 190 | const definition = c.definition[0]; 191 | const sourceColumn = c.columns[0]; 192 | 193 | if(sourceColumn.type != 'identifier' || sourceColumn.variant != 'column') { 194 | return []; 195 | } 196 | 197 | if(definition.references == null || definition.references.columns == null || definition.references.columns.length != 1) { 198 | return []; 199 | } 200 | 201 | const destinationColumn = definition.references.columns[0]; 202 | const foreignTable = config.explicit_main_schema ? ["main", definition.references.name] : [definition.references.name]; 203 | return [[ 204 | `${tableName.join('.')}.${sourceColumn.name}->${definition.references.name}.${destinationColumn.name}`, 205 | { foreign_table: foreignTable, 206 | column_mapping: { 207 | [sourceColumn.name]: destinationColumn.name 208 | } 209 | } 210 | ]]; 211 | }); 212 | }) 213 | } 214 | 215 | function getPrimaryKeyNames(ddl: any): string[] { 216 | if(ddl.type != 'statement' || ddl.variant != 'list') { 217 | throw new Error("Encountered a non-statement or non-list DDL for table."); 218 | } 219 | 220 | return ddl.statement 221 | .filter((ddlStatement: any) => ddlStatement.type === 'statement' && ddlStatement.variant === 'create' && ddlStatement.format === 'table') 222 | .flatMap((createTableDef: any) => { 223 | // Try to discover PKs defined on the column 224 | // (eg 'Id INTEGER PRIMARY KEY NOT NULL') 225 | const pkColumns = 226 | createTableDef.definition 227 | .filter((def: any) => def.type === 'definition' && def.variant === 'column') 228 | .flatMap((columnDef: any) => 229 | columnDef.definition.some((def: any) => def.type === 'constraint' && def.variant === 'primary key') 230 | ? [columnDef.name] 231 | : [] 232 | ); 233 | if (pkColumns.length > 0) 234 | return pkColumns; 235 | 236 | // Try to discover explicit PK constraint defined inside create table DDL 237 | // (eg 'CONSTRAINT [PK_Test] PRIMARY KEY ([Id])') 238 | const pkConstraintColumns = 239 | createTableDef.definition 240 | .filter((def: any) => def.type === 'definition' && def.variant === 'constraint' && def.definition.length === 1 && def.definition[0].type === 'constraint' && def.definition[0].variant === 'primary key') 241 | .flatMap((pkConstraintDef: any) => 242 | pkConstraintDef.columns.flatMap((def: any) => 243 | def.type === 'identifier' && def.variant === 'column' 244 | ? [def.name] 245 | : [] 246 | ) 247 | ); 248 | 249 | return pkConstraintColumns; 250 | }) 251 | } 252 | 253 | export async function getSchema(config: Config, sqlLogger: SqlLogger, schemaRequest: SchemaRequest = {}): Promise { 254 | return await withConnection(config, defaultMode, sqlLogger, async db => { 255 | const detailLevel = schemaRequest.detail_level ?? "everything"; 256 | 257 | const results = await db.query("SELECT * from sqlite_schema"); 258 | const resultsT: TableInfoInternal[] = results as TableInfoInternal[]; 259 | const filtered: TableInfoInternal[] = resultsT.filter(includeTable(config, schemaRequest?.filters?.only_tables)); 260 | const result: TableInfo[] = filtered.map(formatTableInfo(config, detailLevel)); 261 | 262 | return { 263 | tables: result 264 | }; 265 | }); 266 | }; 267 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { ErrorResponseType, TableName, Target } from "@hasura/dc-api-types"; 2 | 3 | export const coerceUndefinedToNull = (v: T | undefined): T | null => v === undefined ? null : v; 4 | 5 | export const coerceUndefinedOrNullToEmptyArray = (v: T[] | undefined | null): T[] => v == null ? [] : v; 6 | 7 | export const coerceUndefinedOrNullToEmptyRecord = (v: Record | undefined | null): Record => v == null ? {} : v; 8 | 9 | export const unreachable = (x: never): never => { throw new Error(`Unreachable code reached! The types lied! 😭 Unexpected value: ${x}`) }; 10 | 11 | export const zip = (arr1: T[], arr2: U[]): [T,U][] => { 12 | const length = Math.min(arr1.length, arr2.length); 13 | const newArray = Array(length); 14 | for (let i = 0; i < length; i++) { 15 | newArray[i] = [arr1[i], arr2[i]]; 16 | } 17 | return newArray; 18 | }; 19 | 20 | export const mapObject = (obj: Record, fn: (entry: [string, T]) => [string, U]): Record => { 21 | return Object.fromEntries(Object.entries(obj).map(fn)); 22 | } 23 | 24 | export const mapObjectToArray = (obj: Record, fn: (entry: [string, T], index: number) => U): Array => { 25 | return Object.entries(obj).map(fn); 26 | } 27 | 28 | export const crossProduct = (arr1: T[], arr2: U[]): [T,U][] => { 29 | return arr1.flatMap(a1 => arr2.map<[T,U]>(a2 => [a1, a2])); 30 | }; 31 | 32 | export function last(x: T[]): T { 33 | return x[x.length - 1]; 34 | } 35 | 36 | export function logDeep(msg: string, myObject: unknown): void { 37 | const util = require('util'); 38 | console.log(msg, util.inspect(myObject, {showHidden: true, depth: null, colors: true})); 39 | } 40 | 41 | export function isEmptyObject(obj: Record): boolean { 42 | return Object.keys(obj).length === 0; 43 | } 44 | 45 | /** 46 | * Usage: `await delay(5000)` 47 | * 48 | * @param ms 49 | * @returns 50 | */ 51 | export function delay(ms: number): Promise { 52 | return new Promise( resolve => setTimeout(resolve, ms) ); 53 | } 54 | 55 | export const tableNameEquals = (tableName1: TableName) => (target: Target): boolean => { 56 | if(target.type != 'table') { 57 | return false; 58 | } 59 | return stringArrayEquals(tableName1)(target.name); 60 | } 61 | 62 | export const tableToTarget = (tableName: TableName): Target => { 63 | return { 64 | type: 'table', 65 | name: tableName 66 | } 67 | } 68 | 69 | export const stringArrayEquals = (arr1: string[]) => (arr2: string[]): boolean => { 70 | if (arr1.length !== arr2.length) 71 | return false; 72 | 73 | return zip(arr1, arr2).every(([n1, n2]) => n1 === n2); 74 | } 75 | 76 | export class ErrorWithStatusCode extends Error { 77 | code: number; 78 | type: ErrorResponseType; 79 | details: Record; 80 | constructor(message: string, code: number, details: Record) { 81 | super(message); 82 | this.code = code; 83 | this.type = 'uncaught-error'; 84 | this.details = details; 85 | } 86 | public static mutationPermissionCheckFailure(message: string, details: Record): ErrorWithStatusCode { 87 | const cls = new ErrorWithStatusCode(message, 400, details); 88 | cls.type = 'mutation-permission-check-failure'; 89 | return cls; 90 | } 91 | } 92 | 93 | /** 94 | * @param inputSequence 95 | * @param asyncFn 96 | * @returns Promise> 97 | * 98 | * This function exists to sequence promise generating inputs and a matching function. 99 | * Promise.all executes in parallel which is not always desired behaviour. 100 | */ 101 | export async function asyncSequenceFromInputs(inputSequence: Input[], asyncFn: (input: Input) => Promise): Promise> { 102 | const results = []; 103 | for (const input of inputSequence) { 104 | results.push(await asyncFn(input)); 105 | } 106 | return results; 107 | } -------------------------------------------------------------------------------- /test/TESTING.md: -------------------------------------------------------------------------------- 1 | # Setting up agent/db 2 | 3 | This will create an agent and simple sqlite db for you to be able to interact with in the HGE Console 4 | 5 | To finish this setup, you will need a working local Console, so do that first. 6 | 7 | 1. `cd` into `dc-agents/sqlite` and run 8 | 9 | ```console 10 | docker build . -t sqlite-agent 11 | ``` 12 | 13 | 2. `cd` into `dc-agents/sqlite/test` and run 14 | 15 | ```console 16 | docker compose up -d 17 | ``` 18 | 19 | 3. From the Console, go to **Settings** -> **Feature Flags** and enable **Experimental features for GDC** 20 | 21 | 4. From the **Data** tab, click **Manage** and click **Add Agent**. 22 | 5. Name your agent `sqlite agent` and depending on your docker setup, for URL use `http://localhost:8100` or `http://host.docker.internal:8100`. If you're not sure, try both and see which works! 23 | 24 | ![Screen Shot 2022-10-07 at 11 06 59 AM](https://user-images.githubusercontent.com/49927862/194598623-5dad962f-a1b0-4db6-9b97-66e71000e344.png) 25 | 26 | 6. Aftering adding the agent, click `Connect Database` and for the **Data Source Driver** choose `sqlite agent` from the dropdown menu. 27 | 7. For **Database Display Name** type in `sqlite-test` and for **db** type in `/chinook.db` and click `Connect Database` 28 | 29 | ![Screen Shot 2022-10-07 at 11 16 34 AM](https://user-images.githubusercontent.com/49927862/194600350-8131459e-cd91-4ac8-9fcc-3d1b2e491a1f.png) 30 | 31 | You should now have this new databse listed on the left: ![Screen Shot 2022-10-07 at 11 12 52 AM](https://user-images.githubusercontent.com/49927862/194599628-952d61e7-1ab8-4c25-8aa2-a9883b9fe6bb.png) 32 | -------------------------------------------------------------------------------- /test/chinook.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/sqlite-dataconnector-agent/ffdd864b4ac41d0b05cbf98576e82a7e17decba4/test/chinook.db -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sqlite_agent: 3 | container_name: 'sqlite_agent' 4 | image: 'sqlite-agent' 5 | network_mode: 'bridge' 6 | ports: 7 | - '8100:8100' 8 | volumes: 9 | - ./chinook.db:/chinook.db 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "resolveJsonModule": true, 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ] 10 | } 11 | --------------------------------------------------------------------------------