Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06e97372f0 | |||
| b806185565 | |||
| bdb0963c8a | |||
| d1cdf36ee5 | |||
| b1e4554d97 | |||
| 68804d37c7 | |||
| 5e05baef6c | |||
| 2f370afd45 | |||
| 2879d8b4bd | |||
| 9ea81c2a31 | |||
| 304109de94 | |||
| 1e885590cb | |||
| dc0108fa9c | |||
| f1b8ec5691 |
+3
-2
@@ -1,8 +1,9 @@
|
||||
.postgres-data
|
||||
.postgres-*
|
||||
.tool-versions
|
||||
.env
|
||||
.envrc
|
||||
.swc
|
||||
node_modules
|
||||
ca-cert*
|
||||
|
||||
dist
|
||||
dist
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Multi tenancy sketches
|
||||
|
||||
This project is a synthetic test project for implementing multi tenancy.
|
||||
Its a functional graphql api backend, that is easiest to access from the apollo graphql explorer.
|
||||
The api and the explorer are available by default at `http://localhost:4000` by default,
|
||||
after the app and its dependencies have been started
|
||||
|
||||
## Project setup
|
||||
|
||||
This project is using [`pnpm`](https://pnpm.io/) as its package manager.
|
||||
To start the required databases or other dependencies, run `docker compose up -d`
|
||||
|
||||
## About Postgres setup
|
||||
|
||||
I had to change the `wal_level` on my local postgres instances
|
||||
it is done with running the SQL command below, and restating the database server:
|
||||
|
||||
```sql
|
||||
ALTER SYSTEM SET wal_level = logical;
|
||||
```
|
||||
|
||||
When registering a new region on a DigitalOcean postgres server, the default user doesn't have the required roles to set up a subscription.
|
||||
On DO we can use [aiven-extras](https://github.com/aiven/aiven-extras) to create subs without root access.
|
||||
The current branch is utilizing just that. But it needs a setup step executed on each database, that is registered as a region
|
||||
|
||||
Run this in a `psql` shell
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION aiven_extras;
|
||||
```
|
||||
|
||||
Note: Postgres subscriptions (which we use) in the same db server don't work that easily; easiest way to get things going is to set up multiple db servers locally.
|
||||
|
||||
## Project description
|
||||
|
||||
The app has these basic concepts:
|
||||
|
||||
### User
|
||||
|
||||
A user of the system (obviously). User authn is not implemented, authz is very simplified.
|
||||
|
||||
### Resource
|
||||
|
||||
This is an abstract object representing a project, that multiple users might work on.
|
||||
The notion of work on is currently implemented as the comment create action.
|
||||
A resource might belong to an organization or belong to the default (null) organization.
|
||||
|
||||
### Comment
|
||||
|
||||
A text note, that belongs to a given resource, created by a user.
|
||||
|
||||
### Region
|
||||
|
||||
A geo-located data storage region, currently implemented as a PostgresSQL database server.
|
||||
When providing a connection url to a region, make sure to not include a database name or any trailing `/`-s in the url.
|
||||
|
||||
### Organization
|
||||
|
||||
A collection of users and an owner of resource. Any user may create organizations.
|
||||
Organizations may be granted access to any given region. That action creates a new database in the region DB server. migrates it to the latest DB schema and sets up user and resource publish and subscribe mechanisms.
|
||||
|
||||
## Steps to flex this POC
|
||||
|
||||
Using the exposed graphql explorer, you can go ahead and
|
||||
|
||||
- create a user
|
||||
- create an organisation
|
||||
- add the user to the organisation
|
||||
- create regions & associate them with an organisation
|
||||
- create a resource in the default organisation, or for a specific organisation & region
|
||||
- etc.
|
||||
@@ -11,3 +11,25 @@ services:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_main
|
||||
|
||||
region-1:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- 5455:5432
|
||||
volumes:
|
||||
- ./.postgres-region-1:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_main
|
||||
|
||||
region-2:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- 5456:5432
|
||||
volumes:
|
||||
- ./.postgres-region-2:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_main
|
||||
|
||||
+20
-2
@@ -1,8 +1,26 @@
|
||||
export default {
|
||||
import { Knex } from 'knex'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
console.log(`foobar ${process.env.POSTGRES_CA_CERT_PATH}`)
|
||||
|
||||
const config: Knex.Config = {
|
||||
client: 'pg',
|
||||
connection: process.env.POSTGRES_URL,
|
||||
connection: {
|
||||
connectionString: process.env.POSTGRES_URL,
|
||||
ssl: process.env.POSTGRES_CA_CERT_PATH
|
||||
? {
|
||||
ca: fs.readFileSync(
|
||||
path.resolve(__dirname, process.env.POSTGRES_CA_CERT_PATH)
|
||||
),
|
||||
rejectUnauthorized: true
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
migrations: {
|
||||
directory: 'src/migrations',
|
||||
extension: 'ts'
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
+5
-2
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "src/app.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "vitest",
|
||||
"lint": "ts-standard",
|
||||
"tsx": "tsx",
|
||||
"lint:fix": "ts-standard --fix",
|
||||
@@ -26,10 +26,13 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-standard": "^12.0.2",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.0",
|
||||
"crypto-random-string": "^3.0.0",
|
||||
"dataloader": "^2.2.2",
|
||||
"dotenv": "^16.4.1",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-scalars": "^1.22.4",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
cert = Path("./ca-cert").read_text()
|
||||
|
||||
cert_json = json.dumps({"cert": cert})
|
||||
|
||||
Path("./ca-cert.json").write_text(cert_json)
|
||||
Generated
+587
@@ -8,6 +8,12 @@ dependencies:
|
||||
'@apollo/server':
|
||||
specifier: ^4.10.0
|
||||
version: 4.10.0(graphql@16.8.1)
|
||||
crypto-random-string:
|
||||
specifier: ^3.0.0
|
||||
version: 3.3.1
|
||||
dataloader:
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
dotenv:
|
||||
specifier: ^16.4.1
|
||||
version: 16.4.1
|
||||
@@ -58,6 +64,9 @@ devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
vitest:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@20.11.16)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -563,6 +572,13 @@ packages:
|
||||
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
|
||||
dev: true
|
||||
|
||||
/@jest/schemas@29.6.3:
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
dev: true
|
||||
|
||||
/@josephg/resolvable@1.0.1:
|
||||
resolution: {integrity: sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==}
|
||||
dev: false
|
||||
@@ -647,6 +663,114 @@ packages:
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
dev: false
|
||||
|
||||
/@rollup/rollup-android-arm-eabi@4.9.6:
|
||||
resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-android-arm64@4.9.6:
|
||||
resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-darwin-arm64@4.9.6:
|
||||
resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-darwin-x64@4.9.6:
|
||||
resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-arm-gnueabihf@4.9.6:
|
||||
resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-arm64-gnu@4.9.6:
|
||||
resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-arm64-musl@4.9.6:
|
||||
resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-riscv64-gnu@4.9.6:
|
||||
resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-x64-gnu@4.9.6:
|
||||
resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-linux-x64-musl@4.9.6:
|
||||
resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-win32-arm64-msvc@4.9.6:
|
||||
resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-win32-ia32-msvc@4.9.6:
|
||||
resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@rollup/rollup-win32-x64-msvc@4.9.6:
|
||||
resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@sinclair/typebox@0.27.8:
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
dev: true
|
||||
|
||||
/@tsconfig/node10@1.0.9:
|
||||
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
|
||||
dev: true
|
||||
@@ -676,6 +800,10 @@ packages:
|
||||
'@types/node': 20.11.16
|
||||
dev: false
|
||||
|
||||
/@types/estree@1.0.5:
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
dev: true
|
||||
|
||||
/@types/express-serve-static-core@4.17.42:
|
||||
resolution: {integrity: sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==}
|
||||
dependencies:
|
||||
@@ -1023,6 +1151,45 @@ packages:
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
dev: true
|
||||
|
||||
/@vitest/expect@1.2.2:
|
||||
resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==}
|
||||
dependencies:
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
chai: 4.4.1
|
||||
dev: true
|
||||
|
||||
/@vitest/runner@1.2.2:
|
||||
resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==}
|
||||
dependencies:
|
||||
'@vitest/utils': 1.2.2
|
||||
p-limit: 5.0.0
|
||||
pathe: 1.1.2
|
||||
dev: true
|
||||
|
||||
/@vitest/snapshot@1.2.2:
|
||||
resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==}
|
||||
dependencies:
|
||||
magic-string: 0.30.6
|
||||
pathe: 1.1.2
|
||||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/@vitest/spy@1.2.2:
|
||||
resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==}
|
||||
dependencies:
|
||||
tinyspy: 2.2.0
|
||||
dev: true
|
||||
|
||||
/@vitest/utils@1.2.2:
|
||||
resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==}
|
||||
dependencies:
|
||||
diff-sequences: 29.6.3
|
||||
estree-walker: 3.0.3
|
||||
loupe: 2.3.7
|
||||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
dev: true
|
||||
@@ -1075,6 +1242,11 @@ packages:
|
||||
color-convert: 2.0.1
|
||||
dev: true
|
||||
|
||||
/ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1172,6 +1344,10 @@ packages:
|
||||
is-shared-array-buffer: 1.0.2
|
||||
dev: true
|
||||
|
||||
/assertion-error@1.1.0:
|
||||
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
|
||||
dev: true
|
||||
|
||||
/async-retry@1.3.3:
|
||||
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
||||
dependencies:
|
||||
@@ -1258,6 +1434,11 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/call-bind@1.0.5:
|
||||
resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
|
||||
dependencies:
|
||||
@@ -1270,6 +1451,19 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/chai@4.4.1:
|
||||
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
assertion-error: 1.1.0
|
||||
check-error: 1.0.3
|
||||
deep-eql: 4.1.3
|
||||
get-func-name: 2.0.2
|
||||
loupe: 2.3.7
|
||||
pathval: 1.1.1
|
||||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1278,6 +1472,12 @@ packages:
|
||||
supports-color: 7.2.0
|
||||
dev: true
|
||||
|
||||
/check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/chokidar@3.5.3:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -1395,6 +1595,17 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/crypto-random-string@3.3.1:
|
||||
resolution: {integrity: sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
type-fest: 0.8.1
|
||||
dev: false
|
||||
|
||||
/dataloader@2.2.2:
|
||||
resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==}
|
||||
dev: false
|
||||
|
||||
/date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
@@ -1436,6 +1647,13 @@ packages:
|
||||
ms: 2.1.2
|
||||
supports-color: 5.5.0
|
||||
|
||||
/deep-eql@4.1.3:
|
||||
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
dev: true
|
||||
@@ -1472,6 +1690,11 @@ packages:
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: false
|
||||
|
||||
/diff-sequences@29.6.3:
|
||||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/diff@4.0.2:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -1969,6 +2192,12 @@ packages:
|
||||
engines: {node: '>=4.0'}
|
||||
dev: true
|
||||
|
||||
/estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
dev: true
|
||||
|
||||
/esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1979,6 +2208,21 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/execa@8.0.1:
|
||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||
engines: {node: '>=16.17'}
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
get-stream: 8.0.1
|
||||
human-signals: 5.0.0
|
||||
is-stream: 3.0.0
|
||||
merge-stream: 2.0.0
|
||||
npm-run-path: 5.2.0
|
||||
onetime: 6.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-final-newline: 3.0.0
|
||||
dev: true
|
||||
|
||||
/express@4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@@ -2171,6 +2415,10 @@ packages:
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
dev: true
|
||||
|
||||
/get-func-name@2.0.2:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
dev: true
|
||||
|
||||
/get-intrinsic@1.2.2:
|
||||
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
|
||||
dependencies:
|
||||
@@ -2189,6 +2437,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/get-stream@8.0.1:
|
||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||
engines: {node: '>=16'}
|
||||
dev: true
|
||||
|
||||
/get-symbol-description@1.0.0:
|
||||
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2334,6 +2587,11 @@ packages:
|
||||
toidentifier: 1.0.1
|
||||
dev: false
|
||||
|
||||
/human-signals@5.0.0:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
dev: true
|
||||
|
||||
/iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2523,6 +2781,11 @@ packages:
|
||||
call-bind: 1.0.5
|
||||
dev: true
|
||||
|
||||
/is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/is-string@1.0.7:
|
||||
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2613,6 +2876,10 @@ packages:
|
||||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/jsonc-parser@3.2.1:
|
||||
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==}
|
||||
dev: true
|
||||
|
||||
/jsx-ast-utils@3.3.5:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -2700,6 +2967,14 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/local-pkg@0.5.0:
|
||||
resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
mlly: 1.5.0
|
||||
pkg-types: 1.0.3
|
||||
dev: true
|
||||
|
||||
/locate-path@3.0.0:
|
||||
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2749,6 +3024,12 @@ packages:
|
||||
js-tokens: 4.0.0
|
||||
dev: true
|
||||
|
||||
/loupe@2.3.7:
|
||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2761,6 +3042,13 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.6:
|
||||
resolution: {integrity: sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: true
|
||||
@@ -2774,6 +3062,10 @@ packages:
|
||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||
dev: false
|
||||
|
||||
/merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
dev: true
|
||||
|
||||
/merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2810,6 +3102,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
@@ -2827,6 +3124,15 @@ packages:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: true
|
||||
|
||||
/mlly@1.5.0:
|
||||
resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==}
|
||||
dependencies:
|
||||
acorn: 8.11.3
|
||||
pathe: 1.1.2
|
||||
pkg-types: 1.0.3
|
||||
ufo: 1.3.2
|
||||
dev: true
|
||||
|
||||
/ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: false
|
||||
@@ -2837,6 +3143,12 @@ packages:
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
/nanoid@3.3.7:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/natural-compare-lite@1.4.0:
|
||||
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
|
||||
dev: true
|
||||
@@ -2895,6 +3207,13 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/npm-run-path@5.2.0:
|
||||
resolution: {integrity: sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dependencies:
|
||||
path-key: 4.0.0
|
||||
dev: true
|
||||
|
||||
/object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2973,6 +3292,13 @@ packages:
|
||||
wrappy: 1.0.2
|
||||
dev: true
|
||||
|
||||
/onetime@6.0.0:
|
||||
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
mimic-fn: 4.0.0
|
||||
dev: true
|
||||
|
||||
/optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3006,6 +3332,13 @@ packages:
|
||||
yocto-queue: 1.0.0
|
||||
dev: true
|
||||
|
||||
/p-limit@5.0.0:
|
||||
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
yocto-queue: 1.0.0
|
||||
dev: true
|
||||
|
||||
/p-locate@3.0.0:
|
||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3081,6 +3414,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/path-key@4.0.0:
|
||||
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
@@ -3093,6 +3431,14 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
dev: true
|
||||
|
||||
/pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
dev: true
|
||||
|
||||
/pg-cloudflare@1.1.1:
|
||||
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
|
||||
requiresBuild: true
|
||||
@@ -3157,6 +3503,10 @@ packages:
|
||||
split2: 4.2.0
|
||||
dev: false
|
||||
|
||||
/picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
dev: true
|
||||
|
||||
/picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@@ -3183,6 +3533,23 @@ packages:
|
||||
load-json-file: 7.0.1
|
||||
dev: true
|
||||
|
||||
/pkg-types@1.0.3:
|
||||
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
||||
dependencies:
|
||||
jsonc-parser: 3.2.1
|
||||
mlly: 1.5.0
|
||||
pathe: 1.1.2
|
||||
dev: true
|
||||
|
||||
/postcss@8.4.33:
|
||||
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3210,6 +3577,15 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.2.0
|
||||
dev: true
|
||||
|
||||
/prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
dependencies:
|
||||
@@ -3265,6 +3641,10 @@ packages:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
|
||||
/react-is@18.2.0:
|
||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||
dev: true
|
||||
|
||||
/readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -3362,6 +3742,29 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rollup@4.9.6:
|
||||
resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.9.6
|
||||
'@rollup/rollup-android-arm64': 4.9.6
|
||||
'@rollup/rollup-darwin-arm64': 4.9.6
|
||||
'@rollup/rollup-darwin-x64': 4.9.6
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.9.6
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.9.6
|
||||
'@rollup/rollup-linux-arm64-musl': 4.9.6
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.9.6
|
||||
'@rollup/rollup-linux-x64-gnu': 4.9.6
|
||||
'@rollup/rollup-linux-x64-musl': 4.9.6
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.9.6
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.9.6
|
||||
'@rollup/rollup-win32-x64-msvc': 4.9.6
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
dependencies:
|
||||
@@ -3501,6 +3904,15 @@ packages:
|
||||
get-intrinsic: 1.2.2
|
||||
object-inspect: 1.13.1
|
||||
|
||||
/siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
dev: true
|
||||
|
||||
/signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/simple-update-notifier@2.0.0:
|
||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3513,6 +3925,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/spawn-command@0.0.2:
|
||||
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
|
||||
dev: true
|
||||
@@ -3522,6 +3939,10 @@ packages:
|
||||
engines: {node: '>= 10.x'}
|
||||
dev: false
|
||||
|
||||
/stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
dev: true
|
||||
|
||||
/standard-engine@15.1.0:
|
||||
resolution: {integrity: sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -3537,6 +3958,10 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/std-env@3.7.0:
|
||||
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
|
||||
dev: true
|
||||
|
||||
/string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3597,11 +4022,22 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/strip-final-newline@3.0.0:
|
||||
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/strip-literal@1.3.0:
|
||||
resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==}
|
||||
dependencies:
|
||||
acorn: 8.11.3
|
||||
dev: true
|
||||
|
||||
/supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3640,6 +4076,20 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/tinybench@2.6.0:
|
||||
resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
|
||||
dev: true
|
||||
|
||||
/tinypool@0.8.2:
|
||||
resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/tinyspy@2.2.0:
|
||||
resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -3778,6 +4228,11 @@ packages:
|
||||
prelude-ls: 1.2.1
|
||||
dev: true
|
||||
|
||||
/type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/type-fest@0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3788,6 +4243,11 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/type-fest@0.8.1:
|
||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3840,6 +4300,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.3.2:
|
||||
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
|
||||
dev: true
|
||||
|
||||
/unbox-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
@@ -3891,6 +4355,120 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/vite-node@1.2.2(@types/node@20.11.16):
|
||||
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.0
|
||||
vite: 5.0.12(@types/node@20.11.16)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite@5.0.12(@types/node@20.11.16):
|
||||
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.11.16
|
||||
esbuild: 0.19.12
|
||||
postcss: 8.4.33
|
||||
rollup: 4.9.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vitest@1.2.2(@types/node@20.11.16):
|
||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': ^1.0.0
|
||||
'@vitest/ui': ^1.0.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.11.16
|
||||
'@vitest/expect': 1.2.2
|
||||
'@vitest/runner': 1.2.2
|
||||
'@vitest/snapshot': 1.2.2
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
acorn-walk: 8.3.2
|
||||
cac: 6.7.14
|
||||
chai: 4.4.1
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.6
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.0
|
||||
std-env: 3.7.0
|
||||
strip-literal: 1.3.0
|
||||
tinybench: 2.6.0
|
||||
tinypool: 0.8.2
|
||||
vite: 5.0.12(@types/node@20.11.16)
|
||||
vite-node: 1.2.2(@types/node@20.11.16)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
@@ -3963,6 +4541,15 @@ packages:
|
||||
isexe: 2.0.0
|
||||
dev: true
|
||||
|
||||
/why-is-node-running@2.2.2:
|
||||
resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
dev: true
|
||||
|
||||
/wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
+2
-13
@@ -3,7 +3,7 @@ import { resolvers } from './resolvers'
|
||||
import { startStandaloneServer } from '@apollo/server/standalone'
|
||||
import { readFileSync } from 'fs'
|
||||
import { typeDefs as scalarTypeDefs } from 'graphql-scalars'
|
||||
import { knex } from './db'
|
||||
import { migrateAll } from './services/databaseManagement'
|
||||
|
||||
const typeDefs = readFileSync('src/schema.graphql', { encoding: 'utf-8' })
|
||||
|
||||
@@ -19,18 +19,7 @@ const startServer = async (): Promise<void> => {
|
||||
listen: { port: 4000 }
|
||||
})
|
||||
|
||||
const plannedMigrations: Array<{ file: string }> = (
|
||||
await knex.migrate.list()
|
||||
)[1]
|
||||
if (plannedMigrations.length > 0) {
|
||||
console.log(
|
||||
`🕰️ planning migrations: ${plannedMigrations
|
||||
.map((m) => m.file)
|
||||
.join(',')}`
|
||||
)
|
||||
}
|
||||
|
||||
await knex.migrate.latest()
|
||||
await migrateAll()
|
||||
|
||||
console.log(`🚀 Server ready at: ${url}`)
|
||||
}
|
||||
|
||||
+3
-4
@@ -2,8 +2,7 @@ import 'dotenv/config'
|
||||
import { parseEnv } from 'znv'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const { POSTGRES_URL } = parseEnv(process.env, {
|
||||
POSTGRES_URL: z.string().min(1)
|
||||
export const { POSTGRES_URL, POSTGRES_CA_CERT_PATH } = parseEnv(process.env, {
|
||||
POSTGRES_URL: z.string().min(1),
|
||||
POSTGRES_CA_CERT_PATH: z.string().min(1).nullish()
|
||||
})
|
||||
|
||||
console.log([POSTGRES_URL].join(', '))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'organizations'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable(tableName, (table) => {
|
||||
table.text('id').primary()
|
||||
table.text('name')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable(tableName)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const regionsTableName = 'regions'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable(regionsTableName, (table) => {
|
||||
table.text('id').primary()
|
||||
table.text('connectionString')
|
||||
})
|
||||
await knex.schema.createTable('organizations_regions', (table) => {
|
||||
table
|
||||
.text('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.notNullable()
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.text('regionId')
|
||||
.references('id')
|
||||
.inTable('regions')
|
||||
.notNullable()
|
||||
.onDelete('cascade')
|
||||
})
|
||||
await knex.schema.createTable('resource_organization_region', (table) => {
|
||||
table
|
||||
.text('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.notNullable()
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.text('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.notNullable()
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.text('regionId')
|
||||
.references('id')
|
||||
.inTable('regions')
|
||||
.notNullable()
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable(regionsTableName)
|
||||
await knex.schema.dropTable('organizations_regions')
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const regionsTableName = 'regions'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.text('name').notNullable().defaultTo('region')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.dropColumn('name')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const regionsTableName = 'regions'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.text('maintenanceDb').notNullable().defaultTo('region')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.dropColumn('maintenanceDb')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'organization_acl'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable(tableName, (table) => {
|
||||
table
|
||||
.string('userId')
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable(tableName)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'organization_resource_acl'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable(tableName, (table) => {
|
||||
table
|
||||
.string('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable(tableName)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'resource_region_organization'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable(tableName, (table) => {
|
||||
table
|
||||
.string('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.onDelete('cascade')
|
||||
.primary()
|
||||
table
|
||||
.string('regionId')
|
||||
.references('id')
|
||||
.inTable('regions')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable(tableName)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const regionsTableName = 'regions'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.dropColumn('maintenanceDb')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(regionsTableName, (table) => {
|
||||
table.text('maintenanceDb').notNullable().defaultTo('region')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'resource_region_organization'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable(tableName)
|
||||
await knex.schema.createTable('resource_region', (table) => {
|
||||
table
|
||||
.string('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.onDelete('cascade')
|
||||
.primary()
|
||||
table
|
||||
.string('regionId')
|
||||
.references('id')
|
||||
.inTable('regions')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
await knex.schema.createTable('resource_organization', (table) => {
|
||||
table
|
||||
.string('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.onDelete('cascade')
|
||||
.primary()
|
||||
table
|
||||
.string('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable('resource_organization')
|
||||
await knex.schema.dropTable('resource_region')
|
||||
await knex.schema.createTable(tableName, (table) => {
|
||||
table
|
||||
.string('resourceId')
|
||||
.references('id')
|
||||
.inTable('resources')
|
||||
.onDelete('cascade')
|
||||
.primary()
|
||||
table
|
||||
.string('regionId')
|
||||
.references('id')
|
||||
.inTable('regions')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('organizationId')
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
.onDelete('cascade')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('regions', (table) => {
|
||||
table.text('sslCaCert').nullable()
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('regions', (table) => {
|
||||
table.dropColumn('sslCaCert')
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
export async function up (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('regions', (table) => {
|
||||
table.unique('name')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down (knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('regions', (table) => {
|
||||
table.dropUnique(['name'])
|
||||
})
|
||||
}
|
||||
+242
-71
@@ -1,76 +1,247 @@
|
||||
import { knex } from "./db";
|
||||
import { UserRecord, Resource, ResourceAcl, Comment } from "./types";
|
||||
import { Knex } from 'knex'
|
||||
import {
|
||||
UserRecord,
|
||||
Resource,
|
||||
ResourceAcl,
|
||||
Comment,
|
||||
Region,
|
||||
OrganizationsRegions,
|
||||
Organization,
|
||||
OrganizationAcl,
|
||||
OrganizationResourceAcl,
|
||||
ResourceRegion
|
||||
} from './types'
|
||||
|
||||
const Users = () => knex<UserRecord>("users");
|
||||
const Resources = () => knex<Resource>("resources");
|
||||
const ResourceAclRepo = () => knex<ResourceAcl>("resource_acl");
|
||||
const Comments = () => knex<Comment>("comments");
|
||||
export class RegionRepo {
|
||||
db: Knex
|
||||
|
||||
export const queryUser = async (userId: string): Promise<UserRecord | null> => {
|
||||
return (await Users().where("id", "=", userId).first()) ?? null;
|
||||
};
|
||||
|
||||
export const queryResource = async (
|
||||
resourceId: string,
|
||||
): Promise<Resource | null> => {
|
||||
return (await Resources().where("id", "=", resourceId).first()) ?? null;
|
||||
};
|
||||
|
||||
export const queryResourceAcl = async ({
|
||||
resourceId,
|
||||
userId,
|
||||
}: {
|
||||
resourceId: string;
|
||||
userId: string;
|
||||
}): Promise<ResourceAcl | null> => {
|
||||
return (
|
||||
(await ResourceAclRepo()
|
||||
.where("userId", "=", userId)
|
||||
.andWhere("resourceId", "=", resourceId)
|
||||
.first()) ?? null
|
||||
);
|
||||
};
|
||||
|
||||
export const countResources = async (userId: string): Promise<number> => {
|
||||
const [rawCount] = await ResourceAclRepo().count().where({ userId });
|
||||
return parseInt(rawCount.count as string);
|
||||
};
|
||||
|
||||
export const queryResources = async ({
|
||||
userId,
|
||||
limit,
|
||||
cursor,
|
||||
}: {
|
||||
userId: string;
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
}) => {
|
||||
const query = Resources()
|
||||
.join("resource_acl", "resources.id", "resource_acl.resourceId")
|
||||
.where({ userId });
|
||||
if (cursor) {
|
||||
query.andWhere("createdAt", "<", cursor);
|
||||
constructor (db: Knex) {
|
||||
this.db = db
|
||||
}
|
||||
return await query.limit(limit);
|
||||
};
|
||||
|
||||
export const countComments = async (resourceId: string): Promise<number> => {
|
||||
const [rawCount] = await Comments().count().where({ resourceId });
|
||||
return parseInt(rawCount.count as string);
|
||||
};
|
||||
|
||||
export const queryComments = async ({
|
||||
resourceId,
|
||||
limit,
|
||||
cursor,
|
||||
}: {
|
||||
resourceId: string;
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
}): Promise<Comment[]> => {
|
||||
const query = Comments().where({ resourceId });
|
||||
if (cursor) {
|
||||
query.andWhere("createdAt", "<", cursor);
|
||||
async saveResource (resource: Resource): Promise<void> {
|
||||
await this.db<Resource>('resources').insert(resource)
|
||||
}
|
||||
return await query.limit(limit);
|
||||
};
|
||||
|
||||
async findResource (resourceId: string): Promise<Resource | null> {
|
||||
return (
|
||||
(await this.db<Resource>('resources')
|
||||
.where({ id: resourceId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async saveComment (comment: Comment): Promise<void> {
|
||||
await this.db<Comment>('comments').insert(comment)
|
||||
}
|
||||
|
||||
async countComments (resourceId: string): Promise<number> {
|
||||
const [rawCount] = await this.db<Comment>('comments')
|
||||
.count()
|
||||
.where({ resourceId })
|
||||
return parseInt(rawCount.count as string)
|
||||
}
|
||||
|
||||
async queryComments ({
|
||||
resourceId,
|
||||
limit,
|
||||
cursor
|
||||
}: {
|
||||
resourceId: string
|
||||
limit: number
|
||||
cursor: string | null
|
||||
}): Promise<Comment[]> {
|
||||
const query = this.db<Comment>('comments').where({ resourceId })
|
||||
if (cursor) {
|
||||
query.andWhere('createdAt', '<', cursor)
|
||||
}
|
||||
return await query.limit(limit)
|
||||
}
|
||||
}
|
||||
|
||||
export class MainRepo extends RegionRepo {
|
||||
async findUser (userId: string): Promise<UserRecord | null> {
|
||||
return (
|
||||
(await this.db<UserRecord>('users').where('id', '=', userId).first()) ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async queryUsers (): Promise<UserRecord[]> {
|
||||
return await this.db<UserRecord>('users').select()
|
||||
}
|
||||
|
||||
async saveUser (user: UserRecord): Promise<void> {
|
||||
await this.db<UserRecord>('users').insert(user)
|
||||
}
|
||||
|
||||
async getUsersResourceAcl ({
|
||||
resourceId,
|
||||
userId
|
||||
}: ResourceAcl): Promise<ResourceAcl | null> {
|
||||
return (
|
||||
(await this.db<ResourceAcl>('resource_acl')
|
||||
.where({ userId, resourceId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async saveResourceAcl (resourceAcl: ResourceAcl): Promise<void> {
|
||||
await this.db<ResourceAcl>('resource_acl').insert(resourceAcl)
|
||||
}
|
||||
|
||||
async countUsersResources (userId: string): Promise<number> {
|
||||
const [rawCount] = await this.db<ResourceAcl>('resource_acl')
|
||||
.count()
|
||||
.where({ userId })
|
||||
return parseInt(rawCount.count as string)
|
||||
}
|
||||
|
||||
async findUsersResource ({
|
||||
resourceId,
|
||||
userId
|
||||
}: ResourceAcl): Promise<ResourceAcl | null> {
|
||||
return (
|
||||
(await this.db<ResourceAcl>('resource_acl')
|
||||
.where({ userId, resourceId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async queryResources ({
|
||||
userId,
|
||||
limit,
|
||||
cursor
|
||||
}: {
|
||||
userId: string
|
||||
limit: number
|
||||
cursor: string | null
|
||||
}): Promise<Resource[]> {
|
||||
let query = this.db<Resource & ResourceAcl>('resources')
|
||||
.join('resource_acl', 'resources.id', 'resource_acl.resourceId')
|
||||
.where({ userId })
|
||||
if (cursor) {
|
||||
query = query.andWhere('createdAt', '<', cursor)
|
||||
}
|
||||
const items = await query.orderBy('createdAt', 'desc').limit(limit)
|
||||
return items
|
||||
}
|
||||
|
||||
async countResourceComments (resourceId: string): Promise<number> {
|
||||
const [rawCount] = await this.db<Comment>('comments')
|
||||
.count()
|
||||
.where({ resourceId })
|
||||
return parseInt(rawCount.count as string)
|
||||
}
|
||||
|
||||
async queryComments ({
|
||||
resourceId,
|
||||
limit,
|
||||
cursor
|
||||
}: {
|
||||
resourceId: string
|
||||
limit: number
|
||||
cursor: string | null
|
||||
}): Promise<Comment[]> {
|
||||
let query = this.db<Comment>('comments').where({ resourceId })
|
||||
if (cursor) {
|
||||
query = query.andWhere('createdAt', '<', cursor)
|
||||
}
|
||||
return await query.orderBy('createdAt', 'desc').limit(limit)
|
||||
}
|
||||
|
||||
async queryRegions (
|
||||
params:
|
||||
| {
|
||||
connectionString?: string | undefined
|
||||
}
|
||||
| undefined = undefined
|
||||
): Promise<Region[]> {
|
||||
const query = this.db<Region>('regions')
|
||||
if ((params != null) && params.connectionString) query.where(params)
|
||||
return await query.select()
|
||||
}
|
||||
|
||||
async findRegion (id: string): Promise<Region | null> {
|
||||
return (await this.db<Region>('regions').where({ id }).first()) ?? null
|
||||
}
|
||||
|
||||
async queryOrganizationsRegions (): Promise<OrganizationsRegions[]> {
|
||||
return await this.db<OrganizationsRegions>('organizations_regions').select()
|
||||
}
|
||||
|
||||
async findOrganizationRegion ({
|
||||
regionId,
|
||||
organizationId
|
||||
}: OrganizationsRegions): Promise<OrganizationsRegions | null> {
|
||||
return (
|
||||
(await this.db<OrganizationsRegions>('organizations_regions')
|
||||
.where({ regionId, organizationId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async saveRegion (region: Region): Promise<void> {
|
||||
await this.db<Region>('regions').insert(region)
|
||||
}
|
||||
|
||||
async saveOrganization (organization: Organization) {
|
||||
await this.db<Organization>('organizations').insert(organization)
|
||||
}
|
||||
|
||||
async findOrganization (id: string): Promise<Organization | null> {
|
||||
return (
|
||||
(await this.db<Organization>('organizations').where({ id }).first()) ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
async queryOrganizations (): Promise<Organization[]> {
|
||||
return await this.db<Organization>('organizations').select()
|
||||
}
|
||||
|
||||
async saveOrganizationRegion (or: OrganizationsRegions): Promise<void> {
|
||||
return await this.db<OrganizationsRegions>('organizations_regions').insert(
|
||||
or
|
||||
)
|
||||
}
|
||||
|
||||
async saveOrganizationAcl (orgAcl: OrganizationAcl): Promise<void> {
|
||||
await this.db<OrganizationsRegions>('organization_acl').insert(orgAcl)
|
||||
}
|
||||
|
||||
async findOrganizationAcl ({
|
||||
userId,
|
||||
organizationId
|
||||
}: OrganizationAcl): Promise<OrganizationAcl | null> {
|
||||
return (
|
||||
(await this.db<OrganizationAcl>('organization_acl')
|
||||
.where({ userId, organizationId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async saveOrganizationResourceAcl (
|
||||
item: OrganizationResourceAcl
|
||||
): Promise<void> {
|
||||
await this.db<OrganizationResourceAcl>('organization_resource_acl').insert(
|
||||
item
|
||||
)
|
||||
}
|
||||
|
||||
async findResourceRegion ({
|
||||
resourceId
|
||||
}: {
|
||||
resourceId: string
|
||||
}): Promise<ResourceRegion | null> {
|
||||
return (
|
||||
(await this.db<ResourceRegion>('resource_region')
|
||||
.where({ resourceId })
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
async saveResourceRegion (item: ResourceRegion): Promise<void> {
|
||||
await this.db<ResourceRegion>('resource_region').insert(item)
|
||||
}
|
||||
}
|
||||
|
||||
+145
-31
@@ -1,63 +1,177 @@
|
||||
import { queryResourceAcl } from "./repositories";
|
||||
import { getUser, getResource, getComments, getResources } from "./services";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { RegionRepo, MainRepo } from './repositories'
|
||||
import { getComments } from './services/comments'
|
||||
import { createResource, getResources } from './services/resources'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import {
|
||||
Resource,
|
||||
ResourceCollection,
|
||||
UserRecord,
|
||||
CommentCollection,
|
||||
PaginationArgs,
|
||||
} from "./types";
|
||||
ResourceCreateArgs,
|
||||
OrganizationsRegions,
|
||||
OrganizationAcl,
|
||||
CommentCreateArgs,
|
||||
UserCreateArgs
|
||||
} from './types'
|
||||
import {
|
||||
createOrganization,
|
||||
registerRegion,
|
||||
getMainRepo,
|
||||
getRegionRepo,
|
||||
getResourceRepo
|
||||
} from './services/databaseManagement'
|
||||
import { authorizeUserOrgRegion } from './services/authz'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
// Resolvers define how to fetch the types defined in your schema.
|
||||
// This resolver retrieves books from the "books" array above.
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
async user(_: unknown, args: { id: string }) {
|
||||
return await getUser(args.id);
|
||||
async users () {
|
||||
return await getMainRepo().queryUsers()
|
||||
},
|
||||
async resource(
|
||||
async user (_: unknown, args: { id: string }) {
|
||||
return await getMainRepo().findUser(args.id)
|
||||
},
|
||||
async resource (
|
||||
_: unknown,
|
||||
args: { id: string; userId: string },
|
||||
args: { id: string, userId: string }
|
||||
): Promise<Resource> {
|
||||
const maybeAcl = await queryResourceAcl({
|
||||
const mainRepo = getMainRepo()
|
||||
const maybeAcl = await mainRepo.getUsersResourceAcl({
|
||||
userId: args.userId,
|
||||
resourceId: args.id,
|
||||
});
|
||||
resourceId: args.id
|
||||
})
|
||||
if (maybeAcl == null) {
|
||||
throw new GraphQLError(
|
||||
"The user doesn't have access to the given resource",
|
||||
{
|
||||
extensions: {
|
||||
code: "FORBIDDEN",
|
||||
},
|
||||
},
|
||||
);
|
||||
code: 'FORBIDDEN'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
const maybeResource = await getResource(args.id);
|
||||
const resourceRepo = await getResourceRepo(args.id)
|
||||
const maybeResource = await resourceRepo.findResource(args.id)
|
||||
if (maybeResource == null) {
|
||||
throw new GraphQLError("Resource not found", {
|
||||
extensions: { code: "RESOURCE_NOT_FOUND" },
|
||||
});
|
||||
throw new GraphQLError('Resource not found', {
|
||||
extensions: { code: 'RESOURCE_NOT_FOUND' }
|
||||
})
|
||||
}
|
||||
return maybeResource;
|
||||
return maybeResource
|
||||
},
|
||||
async organizations () {
|
||||
return await getMainRepo().queryOrganizations()
|
||||
},
|
||||
async regions () {
|
||||
return await getMainRepo().queryRegions()
|
||||
}
|
||||
},
|
||||
User: {
|
||||
async resources(parent: UserRecord, args: PaginationArgs) {
|
||||
return await getResources({ userId: parent.id, ...args });
|
||||
},
|
||||
async resources (parent: UserRecord, args: PaginationArgs) {
|
||||
const mainRepo = getMainRepo()
|
||||
return await getResources(
|
||||
mainRepo.countUsersResources.bind(mainRepo),
|
||||
mainRepo.queryResources.bind(mainRepo)
|
||||
)({ userId: parent.id, ...args })
|
||||
}
|
||||
},
|
||||
Resource: {
|
||||
async comments(
|
||||
async comments (
|
||||
parent: Resource,
|
||||
{ limit, cursor }: PaginationArgs,
|
||||
{ limit, cursor }: PaginationArgs
|
||||
): Promise<CommentCollection> {
|
||||
return await getComments({
|
||||
const resourceRepo = await getResourceRepo(parent.id)
|
||||
return await getComments(
|
||||
resourceRepo.countComments.bind(resourceRepo),
|
||||
resourceRepo.queryComments.bind(resourceRepo)
|
||||
)({
|
||||
resourceId: parent.id,
|
||||
limit,
|
||||
cursor,
|
||||
});
|
||||
},
|
||||
cursor
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
Mutation: {
|
||||
async createUser (
|
||||
_: unknown,
|
||||
{ input: { name } }: { input: UserCreateArgs }
|
||||
) {
|
||||
const id = cryptoRandomString({ length: 10 })
|
||||
await getMainRepo().saveUser({ id, name })
|
||||
return id
|
||||
},
|
||||
async registerRegion (
|
||||
_: unknown,
|
||||
args: {
|
||||
name: string
|
||||
connectionString: string
|
||||
sslCaCert: string | null
|
||||
}
|
||||
) {
|
||||
return await registerRegion(args)
|
||||
},
|
||||
async createOrganization (_: unknown, args: { name: string }) {
|
||||
return await createOrganization(args.name)
|
||||
},
|
||||
async addRegionToOrganization (_: unknown, args: OrganizationsRegions) {
|
||||
await getMainRepo().saveOrganizationRegion(args)
|
||||
},
|
||||
async addUserToOrganization (
|
||||
_: unknown,
|
||||
{ input: args }: { input: OrganizationAcl }
|
||||
) {
|
||||
await getMainRepo().saveOrganizationAcl(args)
|
||||
},
|
||||
async createResource (
|
||||
_: unknown,
|
||||
{ input: args }: { input: ResourceCreateArgs }
|
||||
) {
|
||||
const mainRepo = getMainRepo()
|
||||
await authorizeUserOrgRegion(
|
||||
mainRepo.findOrganizationAcl.bind(mainRepo),
|
||||
mainRepo.findOrganizationRegion.bind(mainRepo)
|
||||
)(args)
|
||||
|
||||
const repo = args.regionId
|
||||
? await getRegionRepo({ regionId: args.regionId })
|
||||
: mainRepo
|
||||
|
||||
const resourceId = await createResource(
|
||||
repo.saveResource.bind(repo),
|
||||
mainRepo.saveResourceAcl.bind(mainRepo)
|
||||
)(args)
|
||||
|
||||
if (args.organizationId) {
|
||||
await mainRepo.saveOrganizationResourceAcl({
|
||||
organizationId: args.organizationId,
|
||||
resourceId
|
||||
})
|
||||
if (args.regionId) {
|
||||
await mainRepo.saveResourceRegion({
|
||||
resourceId,
|
||||
// i know its not null here, the authz function ensures it
|
||||
regionId: args.regionId
|
||||
})
|
||||
}
|
||||
}
|
||||
return resourceId
|
||||
},
|
||||
async addComment (
|
||||
_: unknown,
|
||||
{ input: args }: { input: CommentCreateArgs }
|
||||
) {
|
||||
const mainRepo = getMainRepo()
|
||||
const resourceAcl = await mainRepo.getUsersResourceAcl(args)
|
||||
if (resourceAcl == null) { throw new Error("The user doesn't have access to the given resource") }
|
||||
// 2. get resource db client
|
||||
const resourceRepo = await getResourceRepo(args.resourceId)
|
||||
// 3. save comment to db
|
||||
const id = cryptoRandomString({ length: 10 })
|
||||
const createdAt = new Date()
|
||||
await resourceRepo.saveComment({ id, createdAt, ...args })
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,58 @@ type User {
|
||||
resources(limit: Int! = 10, cursor: String = null): ResourceCollection!
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Region {
|
||||
id: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
user(id: String!): User
|
||||
users: [User!]
|
||||
|
||||
resource(id: String!, userId: String!): Resource
|
||||
|
||||
organizations: [Organization!]
|
||||
regions: [Region!]
|
||||
}
|
||||
|
||||
input ResourceCreateInput {
|
||||
userId: String!
|
||||
name: String!
|
||||
organizationId: String = null
|
||||
regionId: String = null
|
||||
}
|
||||
|
||||
input OrganizationAcl {
|
||||
userId: String!
|
||||
organizationId: String!
|
||||
}
|
||||
|
||||
input CommentInput {
|
||||
userId: String!
|
||||
content: String!
|
||||
resourceId: String!
|
||||
}
|
||||
|
||||
input UserCreateArgs {
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: UserCreateArgs!): String!
|
||||
registerRegion(
|
||||
name: String!
|
||||
connectionString: String!
|
||||
sslCaCert: String
|
||||
): String!
|
||||
createOrganization(name: String!): String!
|
||||
addRegionToOrganization(organizationId: String!, regionId: String!): Boolean
|
||||
addUserToOrganization(input: OrganizationAcl!): Boolean
|
||||
createResource(input: ResourceCreateInput!): String!
|
||||
addComment(input: CommentInput!): String!
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
queryUser,
|
||||
queryResource,
|
||||
countComments,
|
||||
queryComments,
|
||||
countResources,
|
||||
queryResources,
|
||||
} from "./repositories";
|
||||
import {
|
||||
UserRecord,
|
||||
Resource,
|
||||
CommentCollection,
|
||||
PaginationArgs,
|
||||
ResourceCollection,
|
||||
} from "./types";
|
||||
|
||||
export const getUser = async (id: string): Promise<UserRecord | null> => {
|
||||
return await queryUser(id);
|
||||
};
|
||||
|
||||
export const getResource = async (id: string): Promise<Resource | null> => {
|
||||
return await queryResource(id);
|
||||
};
|
||||
|
||||
interface GetResourcesArgs extends PaginationArgs {
|
||||
userId: string;
|
||||
}
|
||||
export const getResources = async (
|
||||
params: GetResourcesArgs,
|
||||
): Promise<ResourceCollection> => {
|
||||
const totalCount = await countResources(params.userId);
|
||||
const items = await queryResources(params);
|
||||
let cursor = null;
|
||||
if (items.length > 0) {
|
||||
cursor = items.slice(-1)[0].createdAt.toISOString();
|
||||
}
|
||||
return {
|
||||
totalCount,
|
||||
items,
|
||||
cursor,
|
||||
};
|
||||
};
|
||||
|
||||
export const getComments = async (params: {
|
||||
resourceId: string;
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
}): Promise<CommentCollection> => {
|
||||
// yes, i should be doing base64 de and encoding with the cursor...
|
||||
const totalCount = await countComments(params.resourceId);
|
||||
const items = await queryComments(params);
|
||||
let cursor = null;
|
||||
if (items.length > 0) {
|
||||
cursor = items.slice(-1)[0].createdAt.toISOString();
|
||||
}
|
||||
return {
|
||||
totalCount,
|
||||
items,
|
||||
cursor,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
OrganizationAcl,
|
||||
OrganizationsRegions,
|
||||
UserOrgRegionArgs
|
||||
} from '../types'
|
||||
|
||||
export const authorizeUserOrgRegion =
|
||||
(
|
||||
orgAclGetter: (params: OrganizationAcl) => Promise<OrganizationAcl | null>,
|
||||
orgRegionGetter: (
|
||||
params: OrganizationsRegions,
|
||||
) => Promise<OrganizationsRegions | null>
|
||||
) =>
|
||||
async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => {
|
||||
if (!organizationId && regionId) { throw new Error("public org doesn't support regions") }
|
||||
if (organizationId) {
|
||||
if (!regionId) throw new Error('organizations can only write to regions')
|
||||
const orgAcl = await orgAclGetter({ organizationId, userId })
|
||||
if (orgAcl == null) { throw new Error("user doesn't have access to this organization") }
|
||||
const orgRegion = await orgRegionGetter({ organizationId, regionId })
|
||||
if (orgRegion == null) { throw new Error('organization doesnt have access to this region') }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CommentCollection, PaginationArgs, Comment } from '../types'
|
||||
|
||||
interface GetCommentsArgs extends PaginationArgs {
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
export const getComments =
|
||||
(
|
||||
countComments: (resourceId: string) => Promise<number>,
|
||||
queryComments: (params: GetCommentsArgs) => Promise<Comment[]>
|
||||
) =>
|
||||
async (params: GetCommentsArgs): Promise<CommentCollection> => {
|
||||
// yes, i should be doing base64 de and encoding with the cursor...
|
||||
const totalCount = await countComments(params.resourceId)
|
||||
const items = await queryComments(params)
|
||||
let cursor = null
|
||||
if (items.length > 0) {
|
||||
cursor = items.slice(-1)[0].createdAt.toISOString()
|
||||
}
|
||||
return {
|
||||
totalCount,
|
||||
items,
|
||||
cursor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { POSTGRES_URL } from '../config'
|
||||
import { RegionRepo, MainRepo } from '../repositories'
|
||||
import knex, { Knex } from 'knex'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
const migrateToLatest = async (db: Knex): Promise<void> => {
|
||||
const plannedMigrations: Array<{ file: string }> = (
|
||||
await db.migrate.list()
|
||||
)[1]
|
||||
if (plannedMigrations.length > 0) {
|
||||
console.log(
|
||||
`🕰️ planning migrations: ${plannedMigrations
|
||||
.map((m) => m.file)
|
||||
.join(',')}`
|
||||
)
|
||||
} else {
|
||||
console.log('no migrations are planned')
|
||||
}
|
||||
// TODO: make sure if a migration fails, all migrations are rolled back
|
||||
await db.migrate.latest()
|
||||
}
|
||||
|
||||
export const migrateAll = async (): Promise<void> => {
|
||||
await migrateToLatest(mainRepo.db)
|
||||
const repos = await getAllRepositories()
|
||||
|
||||
await Promise.all([
|
||||
...repos.map(async (repo) => await migrateToLatest(repo.db))
|
||||
])
|
||||
}
|
||||
|
||||
const createDatabaseConfig = (
|
||||
connectionString: string,
|
||||
sslCaCert: string | null
|
||||
): Knex.Config => {
|
||||
const config: Knex.Config = {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
connectionString,
|
||||
ssl: sslCaCert
|
||||
? {
|
||||
ca: sslCaCert,
|
||||
rejectUnauthorized: true
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
migrations: {
|
||||
directory: 'src/migrations',
|
||||
extension: 'ts'
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const mainRepo = new MainRepo(knex(createDatabaseConfig(POSTGRES_URL, null)))
|
||||
|
||||
const _repoStore: Map<string, RegionRepo> = new Map()
|
||||
export const getRegionRepo = async ({
|
||||
regionId
|
||||
}: {
|
||||
regionId: string | undefined
|
||||
}): Promise<RegionRepo> => {
|
||||
if (!regionId) return mainRepo
|
||||
const maybeRepo = _repoStore.get(regionId)
|
||||
if (maybeRepo != null) return maybeRepo
|
||||
const maybeRegion = await mainRepo.findRegion(regionId)
|
||||
if (maybeRegion == null) throw Error(`region ${regionId} not found`)
|
||||
const repo = new RegionRepo(
|
||||
knex(
|
||||
createDatabaseConfig(maybeRegion.connectionString, maybeRegion.sslCaCert)
|
||||
)
|
||||
)
|
||||
_repoStore.set(regionId, repo)
|
||||
return repo
|
||||
}
|
||||
|
||||
export const getMainRepo = (): MainRepo => mainRepo
|
||||
|
||||
export const registerRegion = async ({
|
||||
name,
|
||||
connectionString,
|
||||
sslCaCert
|
||||
}: {
|
||||
name: string
|
||||
connectionString: string
|
||||
sslCaCert: string | null
|
||||
}): Promise<string> => {
|
||||
const regions = await mainRepo.queryRegions({ connectionString })
|
||||
if (regions.length > 0) throw new Error('This region is already registered')
|
||||
const id = cryptoRandomString({ length: 10 })
|
||||
const repo = new RegionRepo(
|
||||
knex(createDatabaseConfig(connectionString, sslCaCert))
|
||||
)
|
||||
await migrateToLatest(repo.db)
|
||||
_repoStore.set(id, repo)
|
||||
|
||||
const sslmode = sslCaCert ? 'require' : 'disable'
|
||||
await setUpUserReplication({
|
||||
from: mainRepo.db,
|
||||
to: repo.db,
|
||||
regionName: name,
|
||||
sslmode
|
||||
})
|
||||
await setUpResourceReplication({
|
||||
from: repo.db,
|
||||
to: mainRepo.db,
|
||||
regionName: name,
|
||||
sslmode
|
||||
})
|
||||
|
||||
await mainRepo.saveRegion({
|
||||
id,
|
||||
name,
|
||||
connectionString,
|
||||
sslCaCert
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
export const createOrganization = async (name: string): Promise<string> => {
|
||||
const id = cryptoRandomString({ length: 10 })
|
||||
await mainRepo.saveOrganization({ id, name })
|
||||
return id
|
||||
}
|
||||
|
||||
interface ReplicationArgs {
|
||||
from: Knex
|
||||
to: Knex
|
||||
sslmode: string
|
||||
regionName: string
|
||||
}
|
||||
|
||||
const setUpUserReplication = async ({
|
||||
from,
|
||||
to,
|
||||
sslmode,
|
||||
regionName
|
||||
}: ReplicationArgs): Promise<void> => {
|
||||
// TODO: ensure its created...
|
||||
try {
|
||||
await from.raw('CREATE PUBLICATION userspub FOR TABLE users;')
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) throw err
|
||||
if (!err.message.includes('already exists')) throw err
|
||||
}
|
||||
|
||||
const fromUrl = new URL(from.client.config.connection.connectionString)
|
||||
const fromDbName = fromUrl.pathname.replace('/', '')
|
||||
const subName = `userssub_${regionName}`
|
||||
const rawSqeel = `SELECT * FROM aiven_extras.pg_create_subscription(
|
||||
'${subName}',
|
||||
'dbname=${fromDbName} host=${fromUrl.hostname} port=${fromUrl.port} sslmode=${sslmode} user=${fromUrl.username} password=${fromUrl.password}',
|
||||
'userspub',
|
||||
'${subName}',
|
||||
TRUE,
|
||||
TRUE
|
||||
);`
|
||||
try {
|
||||
await to.raw(rawSqeel)
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) throw err
|
||||
if (!err.message.includes('already exists')) throw err
|
||||
}
|
||||
}
|
||||
|
||||
const setUpResourceReplication = async ({
|
||||
from,
|
||||
to,
|
||||
regionName,
|
||||
sslmode
|
||||
}: ReplicationArgs): Promise<void> => {
|
||||
// TODO: ensure its created...
|
||||
try {
|
||||
await from.raw('CREATE PUBLICATION resourcepub FOR TABLE resources;')
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) throw err
|
||||
if (!err.message.includes('already exists')) throw err
|
||||
}
|
||||
|
||||
const fromUrl = new URL(from.client.config.connection.connectionString)
|
||||
const fromDbName = fromUrl.pathname.replace('/', '')
|
||||
const subName = `resourcesub_${regionName}`
|
||||
const rawSqeel = `SELECT * FROM aiven_extras.pg_create_subscription(
|
||||
'${subName}',
|
||||
'dbname=${fromDbName} host=${fromUrl.hostname} port=${fromUrl.port} sslmode=${sslmode} user=${fromUrl.username} password=${fromUrl.password}',
|
||||
'resourcepub',
|
||||
'${subName}',
|
||||
TRUE,
|
||||
TRUE
|
||||
);`
|
||||
try {
|
||||
await to.raw(rawSqeel)
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) throw err
|
||||
if (!err.message.includes('already exists')) throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const getAllRepositories = async (): Promise<RegionRepo[]> => {
|
||||
const regions = await mainRepo.queryRegions({})
|
||||
const regionRepos = await Promise.all(
|
||||
regions.map(async (region) => await getRegionRepo({ regionId: region.id }))
|
||||
)
|
||||
return [mainRepo, ...regionRepos]
|
||||
}
|
||||
|
||||
export const getResourceRepo = async (
|
||||
resourceId: string
|
||||
): Promise<RegionRepo> => {
|
||||
const resourceRegion = await mainRepo.findResourceRegion({ resourceId })
|
||||
return (resourceRegion != null) ? await getRegionRepo(resourceRegion) : getMainRepo()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import {
|
||||
Resource,
|
||||
PaginationArgs,
|
||||
ResourceCollection,
|
||||
ResourceCreateArgs,
|
||||
ResourceAcl
|
||||
} from '../types'
|
||||
|
||||
interface GetResourcesArgs extends PaginationArgs {
|
||||
userId: string
|
||||
}
|
||||
export const getResources =
|
||||
(
|
||||
countResources: (userId: string) => Promise<number>,
|
||||
queryResources: (params: GetResourcesArgs) => Promise<Resource[]>
|
||||
) =>
|
||||
async (params: GetResourcesArgs): Promise<ResourceCollection> => {
|
||||
const totalCount = await countResources(params.userId)
|
||||
const items = await queryResources(params)
|
||||
let cursor = null
|
||||
if (items.length > 0) {
|
||||
cursor = items.slice(-1)[0].createdAt.toISOString()
|
||||
}
|
||||
return {
|
||||
totalCount,
|
||||
items,
|
||||
cursor
|
||||
}
|
||||
}
|
||||
|
||||
export const createResource =
|
||||
(
|
||||
resourceSaver: (resource: Resource) => Promise<void>,
|
||||
resourceAclSaver: (resourceAcl: ResourceAcl) => Promise<void>
|
||||
) =>
|
||||
async ({ userId, name }: ResourceCreateArgs): Promise<string> => {
|
||||
// 1. if no org, create project in main region, validate that, regionId is null
|
||||
// 2. if org, validate if user has access to the org
|
||||
// 3. if org and region, validate if org has access to region
|
||||
// 4. create resource
|
||||
const id = cryptoRandomString({ length: 10 })
|
||||
const resource = { id, name, createdAt: new Date() }
|
||||
await resourceSaver(resource)
|
||||
await resourceAclSaver({ resourceId: id, userId })
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { knex } from "../../db";
|
||||
import { Knex } from "knex";
|
||||
|
||||
type Thing = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
// talk to the DB
|
||||
const repo =
|
||||
(db: Knex<Thing>) =>
|
||||
async (id: string): Promise<Thing | null> => {
|
||||
return (await db.where({ id }).first()) ?? null;
|
||||
};
|
||||
|
||||
// business / domain logic
|
||||
const service =
|
||||
(thingGetter: (id: string) => Promise<Thing | null>) =>
|
||||
async (id: string): Promise<Thing | null> => {
|
||||
return thingGetter(id);
|
||||
};
|
||||
|
||||
const getThingClient = (id: string | undefined): Knex => {
|
||||
if (!id) return knex;
|
||||
return knex;
|
||||
};
|
||||
|
||||
// graphql entry
|
||||
export const resolver = async (args: { id: string }): Promise<Thing> => {
|
||||
const thing = await service(repo(getThingClient(args.id)))(args.id);
|
||||
if (!thing) throw new Error("not found");
|
||||
return thing;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Knex } from "knex";
|
||||
import { knex } from "../../db";
|
||||
type Thing = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ServedThing = {
|
||||
foo: number;
|
||||
} & Thing;
|
||||
|
||||
// talk to the DB
|
||||
const repo =
|
||||
({ db }: { db: Knex<Thing> }) =>
|
||||
async (id: string): Promise<Thing | null> => {
|
||||
return (await db().where({ id }).first()) ?? null;
|
||||
};
|
||||
|
||||
// business / domain logic
|
||||
const service =
|
||||
({ thingGetter }: { thingGetter: (id: string) => Promise<Thing | null> }) =>
|
||||
async (id: string): Promise<ServedThing | null> => {
|
||||
const thing = await thingGetter(id);
|
||||
const foo = 123;
|
||||
return thing ? { ...thing, foo } : null;
|
||||
};
|
||||
|
||||
// graphql entry
|
||||
export const resolver = async (id: string): Promise<ServedThing> => {
|
||||
const thing = await service({ thingGetter: repo({ db: knex }) })(id);
|
||||
if (!thing) throw new Error("not found");
|
||||
return thing;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export type Thing = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ThingRepo = {
|
||||
findThing: (id: string) => Promise<Thing | null>;
|
||||
queryThing: () => Promise<Thing[]>;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Knex } from "knex";
|
||||
import { Thing } from "./domain";
|
||||
|
||||
const findThing =
|
||||
({ db }: { db: Knex }) =>
|
||||
async (id: string): Promise<Thing | null> => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const thingRepo = ({ db }: { db: Knex }) => ({
|
||||
findThing: findThing({ db }),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ServedThing, service } from "./services/service";
|
||||
import { thingRepo } from "./repo";
|
||||
import { knex } from "../../db";
|
||||
|
||||
export const resolver = async (id: string): Promise<ServedThing> => {
|
||||
const thing = await service({
|
||||
thingRepo: thingRepo({ db: knex }),
|
||||
})(id);
|
||||
if (!thing) throw new Error("not found");
|
||||
return thing;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Thing, type ThingRepo } from "../domain";
|
||||
|
||||
export type ServedThing = {
|
||||
foo: number;
|
||||
} & Thing;
|
||||
|
||||
export const service =
|
||||
({ thingRepo }: { thingRepo: Pick<ThingRepo, "findThing"> }) =>
|
||||
async (id: string): Promise<ServedThing | null> => {
|
||||
const thing = await thingRepo.findThing(id);
|
||||
const foo = 123;
|
||||
return thing ? { ...thing, foo } : null;
|
||||
};
|
||||
|
||||
export const service2 = ({
|
||||
thingRepo,
|
||||
}: {
|
||||
thingRepo: Pick<ThingRepo, "queryThing">;
|
||||
}) => {};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { knex } from "../../db";
|
||||
type Thing = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const Things = () => knex<Thing>("things");
|
||||
|
||||
// talk to the DB
|
||||
const repo = async (id: string): Promise<Thing | null> => {
|
||||
return (await Things().where({ id }).first()) ?? null;
|
||||
};
|
||||
|
||||
// business / domain logic
|
||||
const service = async (id: string): Promise<Thing | null> => {
|
||||
return repo(id);
|
||||
};
|
||||
|
||||
// graphql entry
|
||||
export const resolver = async (id: string): Promise<Thing> => {
|
||||
const thing = await service(id);
|
||||
if (!thing) throw new Error("not found");
|
||||
return thing;
|
||||
};
|
||||
+76
-23
@@ -1,46 +1,99 @@
|
||||
export interface Comment {
|
||||
id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
createdAt: Date;
|
||||
resourceId: string;
|
||||
export interface CommentCreateArgs {
|
||||
userId: string
|
||||
content: string
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
export interface Comment extends CommentCreateArgs {
|
||||
id: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface PaginationArgs {
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
limit: number
|
||||
cursor: string | null
|
||||
}
|
||||
|
||||
interface Collection<T> {
|
||||
totalCount: number;
|
||||
cursor: string | null;
|
||||
items: T[];
|
||||
totalCount: number
|
||||
cursor: string | null
|
||||
items: T[]
|
||||
}
|
||||
|
||||
export interface CommentCollection extends Collection<Comment> {}
|
||||
|
||||
export interface UserOrgRegionArgs {
|
||||
userId: string
|
||||
organizationId: string | null
|
||||
regionId: string | null
|
||||
}
|
||||
|
||||
export interface ResourceCreateArgs extends UserOrgRegionArgs {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
id: string
|
||||
name: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export interface ResourceCollection extends Collection<Resource> {}
|
||||
|
||||
export interface UserRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
export interface UserCreateArgs {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface UserRecord extends UserCreateArgs {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface User extends UserRecord {
|
||||
resources: {
|
||||
cursor: string | null;
|
||||
totalCount: number;
|
||||
items: Resource[];
|
||||
};
|
||||
cursor: string | null
|
||||
totalCount: number
|
||||
items: Resource[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResourceAcl {
|
||||
userId: string;
|
||||
resourceId: string;
|
||||
userId: string
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: string
|
||||
name: string
|
||||
connectionString: string
|
||||
sslCaCert: string | null
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface OrganizationAcl {
|
||||
userId: string
|
||||
organizationId: string
|
||||
}
|
||||
|
||||
export interface OrganizationsRegions {
|
||||
organizationId: string
|
||||
regionId: string
|
||||
}
|
||||
|
||||
export interface OrganizationResourceAcl {
|
||||
organizationId: string
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
export interface ResourceRegion {
|
||||
resourceId: string
|
||||
regionId: string
|
||||
}
|
||||
|
||||
export interface ResourceOrganization {
|
||||
resourceId: string
|
||||
organizationId: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, beforeAll, describe, it } from 'vitest'
|
||||
import {
|
||||
getOrganizationRegionsFrom,
|
||||
getRegionsFrom
|
||||
} from '../../src/repositories'
|
||||
import {
|
||||
getMainDbClient,
|
||||
migrateAll
|
||||
} from '../../src/services/databaseManagement'
|
||||
import { Knex } from 'knex'
|
||||
|
||||
describe('regions', () => {
|
||||
let dbClient: Knex
|
||||
|
||||
beforeAll(async () => {
|
||||
dbClient = await getMainDbClient()
|
||||
})
|
||||
it('gets all regions', async () => {
|
||||
const regions = await getRegionsFrom(dbClient)()
|
||||
expect(regions.length).toBeGreaterThan(0)
|
||||
})
|
||||
it('gets organizations regions', async () => {
|
||||
const orgRegions = await getOrganizationRegionsFrom(dbClient)()
|
||||
expect(orgRegions.length).toBeGreaterThan(0)
|
||||
})
|
||||
it('migrates all', async () => {
|
||||
await migrateAll()
|
||||
})
|
||||
})
|
||||
+7
-7
@@ -11,7 +11,7 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
@@ -25,7 +25,7 @@
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
@@ -77,12 +77,12 @@
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
@@ -104,6 +104,6 @@
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
dir: 'tests'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user