diff --git a/docker-compose.yml b/docker-compose.yml index 43bc0b5a..3c81717c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: restart: always volumes: - comify-mongo-data:/data/db + - "${MONGODB_INIT_PATH}:/docker-entrypoint-initdb.d" ports: - "${MONGODB_PORT_NUMBER:-27017}:27017" environment: diff --git a/docker/mongodb/init.js b/docker/mongodb/init.js new file mode 100644 index 00000000..e8e933e7 --- /dev/null +++ b/docker/mongodb/init.js @@ -0,0 +1,9 @@ + +db = db.getSiblingDB('comify'); +db.tenant.insertOne({ + _id: 'localhost', + origins: [ + 'http://localhost:3000', + 'http://localhost:5173' + ] +}); diff --git a/docs/integrations/AUTHENTICATION.md b/docs/integrations/AUTHENTICATION.md index 64aff9c1..1d42319a 100644 --- a/docs/integrations/AUTHENTICATION.md +++ b/docs/integrations/AUTHENTICATION.md @@ -33,7 +33,7 @@ In case of OpenID, additional configuration is required. OPENID_ISSUER="http://localhost:8080/realms/comify" OPENID_CLIENT_ID="openid" OPENID_CLIENT_SECRET="" -OPENID_REDIRECT_URI="http://localhost:3000/rpc/domain/authentication/login" +OPENID_REDIRECT_PATH="/rpc/domain/authentication/login" OPENID_ALLOW_INSECURE_REQUESTS=true ``` diff --git a/eslint.config.js b/eslint.config.js index 4ba6a4cb..314cfb54 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,8 @@ export default tseslint.config( "**/dist/**/*", "**/node_modules/**/*", "**/coverage/**/*", - "**/*config*" + "**/*config*", + "docker" ] }, { diff --git a/example.env b/example.env index 308d1e19..23b2eca3 100644 --- a/example.env +++ b/example.env @@ -23,12 +23,12 @@ MINIO_ACCESS_KEY="development" MINIO_SECRET_KEY="development" # AUTHENTICATION (openid) -AUTHENTICATION_CLIENT_URI="http://localhost:5173/identify" +AUTHENTICATION_CLIENT_PATH="/identify" AUTHENTICATION_IMPLEMENTATION="openid" OPENID_ISSUER="http://localhost:8080/realms/comify" OPENID_CLIENT_ID="openid" OPENID_CLIENT_SECRET="" -OPENID_REDIRECT_URI="http://localhost:3000/rpc/domain/authentication/login" +OPENID_REDIRECT_PATH="/rpc/domain/authentication/login" OPENID_ALLOW_INSECURE_REQUESTS=false # HTTP (fetch) @@ -58,6 +58,7 @@ VALIDATION_IMPLEMENTATION="zod" MONGODB_PORT_NUMBER=27017 MONGODB_ROOT_USERNAME="development" MONGODB_ROOT_PASSWORD="development" +MONGODB_INIT_PATH="./docker/mongodb" # MONGO EXPRESS MONGO_EXPRESS_PORT_NUMBER=8081 diff --git a/package-lock.json b/package-lock.json index 9fa92b37..006aab78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "dayjs": "^1.11.13", - "jitar": "^0.9.3", + "jitar": "^0.10.0", "minio": "^8.0.5", "mongodb": "^6.17.0", "openid-client": "^6.5.1", @@ -22,7 +22,7 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", - "@jitar/plugin-vite": "^0.9.3", + "@jitar/plugin-vite": "^0.10.0", "@types/node": "24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -2209,14 +2209,14 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", - "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.15.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -2224,9 +2224,9 @@ } }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", - "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -2357,13 +2357,13 @@ } }, "node_modules/@jitar/plugin-vite": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@jitar/plugin-vite/-/plugin-vite-0.9.3.tgz", - "integrity": "sha512-S/x4aiL6X4Sv3WCehcpR1H2EtTQjir6clnk5NJLmphMNZhPMuRVvpbLt8JPemXdu1FGGBWfgUI/vzCmUzxiAOQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@jitar/plugin-vite/-/plugin-vite-0.10.0.tgz", + "integrity": "sha512-/BI6EcO/jnPh1OLnuTb8fgj4ttN0ef247o3m9JdIN9SP4yLgcuOib2Ixx/rut9CB/SWGwoRjSd7QLTQ/eoOOzw==", "dev": true, "license": "MIT", "peerDependencies": { - "vite": ">=4.0.0 || >=5.0.0 || >=6.0.0" + "vite": ">=4.0.0 || >=5.0.0 || >=6.0.0 || >=7.0.0" }, "peerDependenciesMeta": { "vite": { @@ -4695,9 +4695,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5767,6 +5767,7 @@ "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -5935,7 +5936,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.3.1", @@ -6062,6 +6062,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -7005,16 +7006,15 @@ } }, "node_modules/jitar": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/jitar/-/jitar-0.9.3.tgz", - "integrity": "sha512-U352BwjuGSt6g7FMyQ1H7tvl66AMZ3JDVObehcEDUeUOifb1+x/OvUKz2X0sfIH8qRRDATjn9lHQ9S4bsTrbFA==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/jitar/-/jitar-0.10.0.tgz", + "integrity": "sha512-a73wwht+ornsOG4OBd+oec0qS1ev0PCPIHrcwfMHI+iF9aOgBtVNR3cbY/ziZGKUawVWcMzSRMZsYSDE4h798Q==", "license": "MIT", "dependencies": { - "dotenv": "^16.5.0", - "express": "^5.0.1", - "fs-extra": "^11.3.0", - "glob": "11.0.1", - "mime-types": "^2.1.35" + "dotenv": "^17.0.1", + "express": "^5.1.0", + "glob": "11.0.3", + "mime-types": "^3.0.1" }, "bin": { "jitar": "dist/cli.js" @@ -7023,27 +7023,25 @@ "node": ">=20.0" } }, - "node_modules/jitar/node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "license": "ISC", + "node_modules/jitar/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/jitar/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "mime-db": "^1.54.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.6" } }, "node_modules/jose": { @@ -7137,6 +7135,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -10191,6 +10190,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" diff --git a/package.json b/package.json index 2a6a114c..8139594c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ ], "dependencies": { "dayjs": "^1.11.13", - "jitar": "^0.9.3", + "jitar": "^0.10.0", "minio": "^8.0.5", "mongodb": "^6.17.0", "openid-client": "^6.5.1", @@ -51,7 +51,7 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", - "@jitar/plugin-vite": "^0.9.3", + "@jitar/plugin-vite": "^0.10.0", "@types/node": "24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", diff --git a/segments/bff.json b/segments/bff.json index 29429e63..000d9812 100644 --- a/segments/bff.json +++ b/segments/bff.json @@ -3,6 +3,8 @@ "./domain/authentication/login": { "default": { "access": "public" } }, "./domain/authentication/logout": { "default": { "access": "public" } }, + "./domain/creator/aggregate": { "default": { "access": "private" }}, + "./domain/creator/getByIdAggregated": { "default": { "access": "private" } }, "./domain/creator/getByNicknameAggregated": { "default": { "access": "public" } }, "./domain/creator/getMeAggregated": { "default": { "access": "public" } }, "./domain/creator/updateFullName": { "default": { "access": "public" } }, @@ -13,18 +15,19 @@ "./domain/creator.metrics/updateFollowing": { "subscriptions": { "access": "private" } }, "./domain/creator.metrics/updatePosts": { "subscriptions": { "access": "private" } }, + "./domain/notification/aggregate": { "default": { "access": "private" } }, "./domain/notification/notify": { "subscriptions": { "access": "private" } }, "./domain/notification/getRecentAggregated": { "default": { "access": "public" } }, - "./domain/notification/getByIdAggregated": { "default": { "access": "public" } }, + "./domain/post/aggregate": { "default": { "access": "private" } }, "./domain/post/create": { "default": { "access": "private" }, "subscribe": { "access": "private" } }, "./domain/post/createWithComic": { "default": { "access": "public" } }, "./domain/post/createWithComment": { "default": { "access": "public" } }, + "./domain/post/getByCreatorAggregated": { "default": { "access": "public" } }, + "./domain/post/getByFollowingAggregated": { "default": { "access": "public" } }, "./domain/post/exploreAggregated": { "default": { "access": "public" } }, "./domain/post/getByIdAggregated": { "default": { "access": "public" } }, "./domain/post/getByParentAggregated": { "default": { "access": "public" } }, - "./domain/post/getByCreatorAggregated": { "default": { "access": "public" } }, - "./domain/post/getByFollowingAggregated": { "default": { "access": "public" } }, "./domain/post/getRecommendedAggregated": { "default": { "access": "public"}}, "./domain/post/remove": { "default": { "access": "public" }, "subscribe": { "access": "private" } }, @@ -34,9 +37,12 @@ "./domain/rating/toggle": { "default": { "access": "public" }, "subscribe": { "access": "private" } }, + "./domain/relation/aggregate": { "default": { "access": "private" }}, "./domain/relation/exploreAggregated": { "default": { "access": "public" } }, "./domain/relation/establish": { "default": { "access": "public" }, "subscribe": { "access": "private" } }, "./domain/relation/getAggregated": { "default": { "access": "public" } }, "./domain/relation/getFollowersAggregated": { "default": { "access": "public" } }, - "./domain/relation/getFollowingAggregated": { "default": { "access": "public" } } + "./domain/relation/getFollowingAggregated": { "default": { "access": "public" } }, + + "./domain/tenant/getByOriginConverted": { "default": { "access": "public" } } } \ No newline at end of file diff --git a/segments/notification.json b/segments/notification.json index 00a16a4b..a6c6a98c 100644 --- a/segments/notification.json +++ b/segments/notification.json @@ -1,6 +1,5 @@ { "./domain/notification/create": { "default": { "access": "protected" } }, - "./domain/notification/getById": { "default": { "access": "protected" } }, "./domain/notification/getByPostId": { "default": { "access": "protected" } }, "./domain/notification/getRecent": { "default": { "access": "protected" } }, "./domain/notification/remove": { "default": { "access": "protected" } } diff --git a/segments/reads.json b/segments/reads.json index a45a91d3..a5e99a36 100644 --- a/segments/reads.json +++ b/segments/reads.json @@ -3,10 +3,14 @@ "./domain/comment/getById": { "default": { "access": "protected" } }, - "./domain/creator/getById": { "default": { "access": "protected" } }, + "./domain/creator/generateNickname/retrieveByNickname": { "default": { "access": "protected" } }, + "./domain/creator/generateNickname/retrieveByStartNickname": { "default": { "access": "protected" } }, "./domain/creator/getByEmail": { "default": { "access": "protected" } }, + "./domain/creator/getById": { "default": { "access": "protected" } }, "./domain/creator/getByNickname": { "default": { "access": "protected" } }, "./domain/creator/getMe": { "default": { "access": "protected" } }, + "./domain/creator/getOthers": { "default": { "access": "private" }}, + "./domain/creator/updateNickname/retrieveByNickname": { "default": { "access": "protected" } }, "./domain/creator.metrics/getByCreator": { "default": { "access": "protected" } }, @@ -28,5 +32,7 @@ "./domain/relation/explore": { "default": { "access": "protected" } }, "./domain/relation/get": { "default": { "access": "protected" } }, "./domain/relation/getFollowers": { "default": { "access": "protected" } }, - "./domain/relation/getFollowing": { "default": { "access": "protected" } } + "./domain/relation/getFollowing": { "default": { "access": "protected" } }, + + "./domain/tenant/getByOrigin": { "default": { "access": "protected" }, "TenantNotFound": { } } } \ No newline at end of file diff --git a/segments/writes.json b/segments/writes.json index c1f09fa1..11e4fa36 100644 --- a/segments/writes.json +++ b/segments/writes.json @@ -6,17 +6,19 @@ "./domain/comment/erase": { "default": { "access": "protected" } }, "./domain/creator/create": { "default": { "access": "protected" } }, + "./domain/creator/erase": { "default": { "access": "protected" } }, "./domain/creator/update": { "default": { "access": "protected" } }, "./domain/creator.metrics/create/insertData": { "default": { "access": "protected" } }, "./domain/creator.metrics/update": { "default": { "access": "protected" } }, - "./domain/image/save": { "default": { "access": "protected" } }, "./domain/image/erase": { "default": { "access": "protected" } }, + "./domain/image/save": { "default": { "access": "protected" } }, "./domain/post/create/insertData": { "default": { "access": "protected" } }, "./domain/post/erase": { "default": { "access": "protected" } }, "./domain/post/remove/deleteData": { "default": { "access": "protected" }}, + "./domain/post/remove/undeleteData": { "default": { "access": "protected" }}, "./domain/post/update": { "default": { "access": "protected" } }, "./domain/post.metrics/create/insertData": { "default": { "access": "protected" } }, diff --git a/services/bff.json b/services/bff.json index 07f49c17..7e8c0c3d 100644 --- a/services/bff.json +++ b/services/bff.json @@ -6,6 +6,11 @@ "tearDown": [ "./integrations/runtime/tearDownBff" ], + "middleware": [ + "./integrations/runtime/originMiddleware", + "./integrations/runtime/authenticationMiddleware", + "./integrations/runtime/tenantMiddleware" + ], "worker": { "gateway": "http://127.0.0.1:2000", diff --git a/services/gateway.json b/services/gateway.json index e0950fe4..b8286672 100644 --- a/services/gateway.json +++ b/services/gateway.json @@ -1,14 +1,5 @@ { "url": "http://127.0.0.1:2000", - "setUp": [ - "./integrations/runtime/setUpGateway" - ], - "tearDown": [ - "./integrations/runtime/tearDownGateway" - ], - "middleware": [ - "./integrations/runtime/authenticationMiddleware" - ], "gateway": { "trustKey": "${JITAR_TRUST_KEY}" diff --git a/services/proxy.json b/services/proxy.json index 456a3edb..d85c89eb 100644 --- a/services/proxy.json +++ b/services/proxy.json @@ -3,6 +3,6 @@ "proxy": { "repository": "http://127.0.0.1:1000", - "gateway": "http://127.0.0.1:2000" + "gateway": "http://127.0.0.1:4000" } } \ No newline at end of file diff --git a/services/standalone.json b/services/standalone.json index 0fd741ce..e93fad1f 100644 --- a/services/standalone.json +++ b/services/standalone.json @@ -2,11 +2,9 @@ "url": "http://127.0.0.1:3000", "setUp": [ "./integrations/runtime/setUpBff", - "./integrations/runtime/setUpWorker", - "./integrations/runtime/setUpGateway" + "./integrations/runtime/setUpWorker" ], "tearDown": [ - "./integrations/runtime/tearDownGateway", "./integrations/runtime/tearDownWorker", "./integrations/runtime/tearDownBff" ], @@ -15,7 +13,9 @@ "./integrations/runtime/databaseHealthCheck" ], "middleware": [ - "./integrations/runtime/authenticationMiddleware" + "./integrations/runtime/originMiddleware", + "./integrations/runtime/authenticationMiddleware", + "./integrations/runtime/tenantMiddleware" ], "standalone": { diff --git a/src/assets/localhost.css b/src/assets/localhost.css new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/authentication/login/login.ts b/src/domain/authentication/login/login.ts index 74a5d451..e943695d 100644 --- a/src/domain/authentication/login/login.ts +++ b/src/domain/authentication/login/login.ts @@ -3,14 +3,16 @@ import type { Identity } from '^/integrations/authentication'; import getCreatorByEmail from '^/domain/creator/getByEmail'; import registerCreator from '^/domain/creator/register'; +import type { Tenant } from '^/domain/tenant'; import type { Requester } from '../types'; -export default async function login(identity: Identity): Promise +export default async function login(tenant: Tenant, identity: Identity): Promise { - const existingCreator = await getCreatorByEmail(identity.email); + const existingCreator = await getCreatorByEmail(tenant.id, identity.email); const loggedInCreator = existingCreator ?? await registerCreator( + tenant.id, identity.name, identity.nickname ?? identity.name, identity.email, diff --git a/src/domain/creator/aggregate/types.ts b/src/domain/creator/aggregate/types.ts index 403b76be..d5ec66c3 100644 --- a/src/domain/creator/aggregate/types.ts +++ b/src/domain/creator/aggregate/types.ts @@ -4,7 +4,7 @@ import type { ImageData } from '^/domain/image'; import type { DataModel } from '../types'; -type AggregatedData = Omit & +type AggregatedData = Omit & { readonly portrait?: ImageData; readonly metrics: metricsData; diff --git a/src/domain/creator/create/create.ts b/src/domain/creator/create/create.ts index 2bd12d49..5665e881 100644 --- a/src/domain/creator/create/create.ts +++ b/src/domain/creator/create/create.ts @@ -4,9 +4,9 @@ import createData from './createData'; import insertData from './insertData'; import validateData from './validateData'; -export default async function create(fullName: string, nickname: string, email: string, portraitId: string | undefined = undefined): Promise +export default async function create(tenantId: string, fullName: string, nickname: string, email: string, portraitId: string | undefined = undefined): Promise { - const data = await createData(fullName, nickname, email, portraitId); + const data = await createData(tenantId, fullName, nickname, email, portraitId); validateData(data); diff --git a/src/domain/creator/create/createData.ts b/src/domain/creator/create/createData.ts index 9b79f0a9..aaed975e 100644 --- a/src/domain/creator/create/createData.ts +++ b/src/domain/creator/create/createData.ts @@ -3,7 +3,7 @@ import { generateId } from '^/integrations/utilities/crypto'; import type { DataModel } from '../types'; -export default async function createData(fullName: string, nickname: string, email: string, portraitId: string | undefined = undefined): Promise +export default async function createData(tenantId: string, fullName: string, nickname: string, email: string, portraitId?: string): Promise { return { id: generateId(), @@ -11,6 +11,7 @@ export default async function createData(fullName: string, nickname: string, ema nickname, email, portraitId: portraitId, + tenantId, joinedAt: new Date().toISOString() }; } diff --git a/src/domain/creator/create/types.ts b/src/domain/creator/create/types.ts index 55312de7..d199269c 100644 --- a/src/domain/creator/create/types.ts +++ b/src/domain/creator/create/types.ts @@ -1,6 +1,6 @@ import type { DataModel } from '../types'; -type ValidationModel = Pick; +type ValidationModel = Pick; export type { ValidationModel }; diff --git a/src/domain/creator/create/validateData.ts b/src/domain/creator/create/validateData.ts index a35d4a88..bf6d6fb7 100644 --- a/src/domain/creator/create/validateData.ts +++ b/src/domain/creator/create/validateData.ts @@ -2,7 +2,7 @@ import type { ValidationSchema } from '^/integrations/validation'; import validator from '^/integrations/validation'; -import { optionalIdValidation } from '^/domain/definitions'; +import { optionalIdValidation, requiredIdValidation } from '^/domain/definitions'; import InvalidCreator from '../InvalidCreator'; import { fullNameValidation } from '../definitions'; @@ -10,6 +10,7 @@ import type { ValidationModel } from './types'; const schema: ValidationSchema = { + tenantId: requiredIdValidation, fullName: fullNameValidation, email: { @@ -22,9 +23,9 @@ const schema: ValidationSchema = portraitId: optionalIdValidation }; -export default function validateData({ fullName, email }: ValidationModel): void +export default function validateData({ tenantId, fullName, email }: ValidationModel): void { - const result = validator.validate({ fullName, email }, schema); + const result = validator.validate({ tenantId, fullName, email }, schema); if (result.invalid) { diff --git a/src/domain/creator/generateNickname/generateNickname.ts b/src/domain/creator/generateNickname/generateNickname.ts index 3a6b315d..ef1cdcee 100644 --- a/src/domain/creator/generateNickname/generateNickname.ts +++ b/src/domain/creator/generateNickname/generateNickname.ts @@ -6,18 +6,18 @@ import retrieveByStartNickname from './retrieveByStartNickname'; const MAX_NICKNAME_NUMBER = 1000; -export default async function generateNickname(nickname: string): Promise +export default async function generateNickname(tenantId: string, nickname: string): Promise { const cleanedNickname = cleanNickname(nickname); - const existingData = await retrieveByNickname(cleanedNickname); + const existingData = await retrieveByNickname(tenantId, cleanedNickname); if (existingData === undefined) { return cleanedNickname; } - const foundData = await retrieveByStartNickname(`${existingData.nickname}_`); + const foundData = await retrieveByStartNickname(tenantId, `${existingData.nickname}_`); if (foundData === undefined) { diff --git a/src/domain/creator/generateNickname/retrieveByNickname.ts b/src/domain/creator/generateNickname/retrieveByNickname.ts index 27201487..be71189a 100644 --- a/src/domain/creator/generateNickname/retrieveByNickname.ts +++ b/src/domain/creator/generateNickname/retrieveByNickname.ts @@ -4,9 +4,12 @@ import database from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function retrieveByNickname(nickname: string): Promise +export default async function retrieveByNickname(tenantId: string, nickname: string): Promise { - const query = { nickname: { EQUALS: nickname } }; + const query = { + tenantId: { 'EQUALS': tenantId }, + nickname: { 'EQUALS': nickname } + }; return database.findRecord(RECORD_TYPE, query) as Promise; } diff --git a/src/domain/creator/generateNickname/retrieveByStartNickname.ts b/src/domain/creator/generateNickname/retrieveByStartNickname.ts index 6e318828..6cb8466a 100644 --- a/src/domain/creator/generateNickname/retrieveByStartNickname.ts +++ b/src/domain/creator/generateNickname/retrieveByStartNickname.ts @@ -1,14 +1,18 @@ -import type { RecordSort} from '^/integrations/database'; +import type { RecordSort } from '^/integrations/database'; import database, { SortDirections } from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function retrieveByStartNickname(nickname: string): Promise +export default async function retrieveByStartNickname(tenantId: string, nickname: string): Promise { - const query = { nickname: { STARTS_WITH: nickname } }; + const query = { + tenantId: { 'EQUALS': tenantId }, + nickname: { 'STARTS_WITH': nickname } + }; + const sort: RecordSort = { 'nickname': SortDirections.DESCENDING }; return database.findRecord(RECORD_TYPE, query, undefined, sort) as Promise; -} +}; diff --git a/src/domain/creator/getByEmail/getByEmail.ts b/src/domain/creator/getByEmail/getByEmail.ts index 35c2e1b7..f303dde9 100644 --- a/src/domain/creator/getByEmail/getByEmail.ts +++ b/src/domain/creator/getByEmail/getByEmail.ts @@ -4,9 +4,12 @@ import database from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function getByEmail(email: string): Promise +export default async function getByEmail(tenantId: string, email: string): Promise { - const query = { email: { EQUALS: email } }; + const query = { + tenantId: { EQUALS: tenantId }, + email: { EQUALS: email } + }; return database.findRecord(RECORD_TYPE, query) as Promise; } diff --git a/src/domain/creator/getById/CreatorNotFound.ts b/src/domain/creator/getById/CreatorNotFound.ts new file mode 100644 index 00000000..fa11b77a --- /dev/null +++ b/src/domain/creator/getById/CreatorNotFound.ts @@ -0,0 +1,10 @@ + +import { NotFound } from '^/integrations/runtime'; + +export default class CreatorNotFound extends NotFound +{ + constructor(tenantId: string, id: string) + { + super(`No creator for id: ${id} and tenant '${tenantId}'`); + } +} diff --git a/src/domain/creator/getById/getById.ts b/src/domain/creator/getById/getById.ts index 954137d2..d4999d3e 100644 --- a/src/domain/creator/getById/getById.ts +++ b/src/domain/creator/getById/getById.ts @@ -1,10 +1,23 @@ -import database from '^/integrations/database'; +import database, { type RecordQuery } from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; +import CreatorNotFound from './CreatorNotFound'; -export default async function getById(id: string): Promise +export default async function getById(tenantId: string, id: string): Promise { - return database.readRecord(RECORD_TYPE, id) as Promise; + const query: RecordQuery = { + tenantId: { 'EQUALS': tenantId }, + id: { 'EQUALS': id } + }; + + const creator = await database.findRecord(RECORD_TYPE, query); + + if (creator === undefined) + { + throw new CreatorNotFound(tenantId, id); + } + + return creator as DataModel; } diff --git a/src/domain/creator/getByIdAggregated/getByIdAggregated.ts b/src/domain/creator/getByIdAggregated/getByIdAggregated.ts index 1615346c..a9d16971 100644 --- a/src/domain/creator/getByIdAggregated/getByIdAggregated.ts +++ b/src/domain/creator/getByIdAggregated/getByIdAggregated.ts @@ -3,9 +3,9 @@ import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getById from '../getById'; -export default async function getByIdAggregated(id: string): Promise +export default async function getByIdAggregated(tenantId: string, id: string): Promise { - const data = await getById(id); + const data = await getById(tenantId, id); return aggregate(data); } diff --git a/src/domain/creator/getByNickname/getByNickname.ts b/src/domain/creator/getByNickname/getByNickname.ts index 41387179..702ab04b 100644 --- a/src/domain/creator/getByNickname/getByNickname.ts +++ b/src/domain/creator/getByNickname/getByNickname.ts @@ -5,16 +5,19 @@ import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; import NicknameNotFound from './NicknameNotFound'; -export default async function getByNickname(nickname: string): Promise +export default async function getByNickname(tenantId: string, nickname: string): Promise { - const query = { nickname: { EQUALS: nickname } }; + const query = { + tenantId: { EQUALS: tenantId }, + nickname: { EQUALS: nickname } + }; - const data = await database.findRecord(RECORD_TYPE, query) as DataModel; + const creator = await database.findRecord(RECORD_TYPE, query); - if (data === undefined) + if (creator === undefined) { throw new NicknameNotFound(nickname); } - return data; + return creator as DataModel; } diff --git a/src/domain/creator/getByNicknameAggregated/getByNicknameAggregated.ts b/src/domain/creator/getByNicknameAggregated/getByNicknameAggregated.ts index f1bc7e17..7c4d61fd 100644 --- a/src/domain/creator/getByNicknameAggregated/getByNicknameAggregated.ts +++ b/src/domain/creator/getByNicknameAggregated/getByNicknameAggregated.ts @@ -1,11 +1,14 @@ +import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; + import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getByNickname from '../getByNickname'; -export default async function getByNicknameAggregated(nickname: string): Promise +export default async function getByNicknameAggregated(tenant: Tenant, requester: Requester, nickname: string): Promise { - const data = await getByNickname(nickname); + const data = await getByNickname(tenant.id, nickname); return aggregate(data); } diff --git a/src/domain/creator/getMe/getMe.ts b/src/domain/creator/getMe/getMe.ts index ed47da31..79f71b3f 100644 --- a/src/domain/creator/getMe/getMe.ts +++ b/src/domain/creator/getMe/getMe.ts @@ -1,10 +1,11 @@ import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import getById from '../getById'; import type { DataModel } from '../types'; -export default async function getMe(requester: Requester): Promise +export default async function getMe(tenant: Tenant, requester: Requester): Promise { - return getById(requester.id); + return getById(tenant.id, requester.id); } diff --git a/src/domain/creator/getMeAggregated/getMeAggregated.ts b/src/domain/creator/getMeAggregated/getMeAggregated.ts index d9850093..1b170185 100644 --- a/src/domain/creator/getMeAggregated/getMeAggregated.ts +++ b/src/domain/creator/getMeAggregated/getMeAggregated.ts @@ -1,13 +1,14 @@ import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getMe from '../getMe'; -export default async function getMeAggregated(requester: Requester): Promise +export default async function getMeAggregated(tenant: Tenant, requester: Requester): Promise { - const data = await getMe(requester); + const data = await getMe(tenant, requester); return aggregate(data); } diff --git a/src/domain/creator/getOthers/getOthers.ts b/src/domain/creator/getOthers/getOthers.ts index 9b1eac38..60e2913d 100644 --- a/src/domain/creator/getOthers/getOthers.ts +++ b/src/domain/creator/getOthers/getOthers.ts @@ -1,14 +1,19 @@ -import type { QueryStatement, RecordQuery, RecordSort} from '^/integrations/database'; +import type { QueryStatement, RecordQuery, RecordSort } from '^/integrations/database'; import database, { SortDirections } from '^/integrations/database'; -import type { SortOrder} from '../definitions'; +import type { SortOrder } from '../definitions'; import { RECORD_TYPE, SortOrders } from '../definitions'; import type { DataModel } from '../types'; -export default async function getOthers(ids: string[], order: SortOrder, limit: number, offset: number, search: string | undefined = undefined): Promise +export default async function getOthers(tenantId: string, ids: string[], order: SortOrder, limit: number, offset: number, search: string | undefined = undefined): Promise { - const defaultQuery: RecordQuery = { id: { NOT_IN: ids } }; + const defaultQuery: RecordQuery = { + 'AND': [ + { tenantId: { EQUALS: tenantId } }, + { id: { NOT_IN: ids } } + ] + }; const searchQuery: RecordQuery = { 'OR': [ { fullName: { CONTAINS: search } }, diff --git a/src/domain/creator/register/register.ts b/src/domain/creator/register/register.ts index f7951e62..477d5363 100644 --- a/src/domain/creator/register/register.ts +++ b/src/domain/creator/register/register.ts @@ -9,20 +9,20 @@ import type { DataModel } from '../types'; import downloadPortrait from './downloadPortrait'; import publish from './publish'; -export default async function register(fullName: string, nickname: string, email: string, portraitUrl: string | undefined = undefined): Promise +export default async function register(tenantId: string, fullName: string, nickname: string, email: string, portraitUrl: string | undefined = undefined): Promise { let data; try { const truncatedFullName = fullName.substring(0, FULL_NAME_MAX_LENGTH); - const generatedNickname = await generateNickname(nickname); + const generatedNickname = await generateNickname(tenantId, nickname); const portraitId = portraitUrl !== undefined ? await downloadPortrait(portraitUrl) : undefined; - data = await create(truncatedFullName, generatedNickname, email, portraitId); + data = await create(tenantId, truncatedFullName, generatedNickname, email, portraitId); await publish(data.id); diff --git a/src/domain/creator/types.ts b/src/domain/creator/types.ts index 27a22866..ff7a5f72 100644 --- a/src/domain/creator/types.ts +++ b/src/domain/creator/types.ts @@ -3,6 +3,7 @@ import type { BaseDataModel, CountOperation } from '../types'; type DataModel = BaseDataModel & { + readonly tenantId: string; readonly fullName: string; readonly nickname: string; readonly email: string; diff --git a/src/domain/creator/updateNickname/retrieveByNickname.ts b/src/domain/creator/updateNickname/retrieveByNickname.ts index 27201487..c4dbc750 100644 --- a/src/domain/creator/updateNickname/retrieveByNickname.ts +++ b/src/domain/creator/updateNickname/retrieveByNickname.ts @@ -4,9 +4,12 @@ import database from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function retrieveByNickname(nickname: string): Promise +export default async function retrieveByNickname(tenantId: string, nickname: string): Promise { - const query = { nickname: { EQUALS: nickname } }; + const query = { + tenantId: { EQUALS: tenantId }, + nickname: { EQUALS: nickname } + }; return database.findRecord(RECORD_TYPE, query) as Promise; } diff --git a/src/domain/creator/updateNickname/updateNickname.ts b/src/domain/creator/updateNickname/updateNickname.ts index 21696e69..48c53f51 100644 --- a/src/domain/creator/updateNickname/updateNickname.ts +++ b/src/domain/creator/updateNickname/updateNickname.ts @@ -1,16 +1,18 @@ import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import cleanNickname from '../cleanNickname'; import update from '../update'; import NicknameAlreadyExists from './NicknameAlreadyExists'; import retrieveByNickname from './retrieveByNickname'; -export default async function updateNickname(requester: Requester, nickname: string): Promise + +export default async function updateNickname(tenant: Tenant, requester: Requester, nickname: string): Promise { const cleanedNickname = cleanNickname(nickname); - const data = await retrieveByNickname(cleanedNickname); + const data = await retrieveByNickname(tenant.id, cleanedNickname); if (data !== undefined) { diff --git a/src/domain/definitions.ts b/src/domain/definitions.ts index 83bc06a8..ddf98127 100644 --- a/src/domain/definitions.ts +++ b/src/domain/definitions.ts @@ -1,6 +1,8 @@ import type { Validation } from '^/integrations/validation'; +export const TENANT_BY_ORIGIN_PATH = 'domain/tenant/getByOriginConverted'; + export const SortOrders = { POPULAR: 'popular', RECENT: 'recent' diff --git a/src/domain/notification/aggregate/aggregate.ts b/src/domain/notification/aggregate/aggregate.ts index ab54dd15..41664b0a 100644 --- a/src/domain/notification/aggregate/aggregate.ts +++ b/src/domain/notification/aggregate/aggregate.ts @@ -2,15 +2,16 @@ import type { Requester } from '^/domain/authentication'; import { default as getPostData } from '^/domain/post/getByIdAggregated'; import getRelationData from '^/domain/relation/getAggregated'; +import type { Tenant } from '^/domain/tenant'; import type { DataModel } from '../types'; import type { AggregatedData } from './types'; -export default async function aggregate(requester: Requester, data: DataModel): Promise +export default async function aggregate(tenant: Tenant, requester: Requester, data: DataModel): Promise { const [relationData, postData] = await Promise.all([ - getRelationData(data.receiverId, data.senderId), - data.postId ? getPostData(requester, data.postId) : Promise.resolve(undefined) + getRelationData(tenant, requester, data.receiverId, data.senderId), + data.postId ? getPostData(tenant, requester, data.postId) : Promise.resolve(undefined) ]); return { diff --git a/src/domain/notification/getById/getById.ts b/src/domain/notification/getById/getById.ts deleted file mode 100644 index 4f4f52db..00000000 --- a/src/domain/notification/getById/getById.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import type { RecordQuery } from '^/integrations/database'; -import database from '^/integrations/database'; - -import { RECORD_TYPE } from '../definitions'; -import type { DataModel } from '../types'; - -export default async function getById(id: string): Promise -{ - const query: RecordQuery = - { - id: { EQUALS: id }, - deleted: { EQUALS: false } - }; - return database.findRecord(RECORD_TYPE, query) as Promise; -} diff --git a/src/domain/notification/getById/index.ts b/src/domain/notification/getById/index.ts deleted file mode 100644 index da399eb0..00000000 --- a/src/domain/notification/getById/index.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export { default } from './getById'; diff --git a/src/domain/notification/getByIdAggregated/getByIdAggregated.ts b/src/domain/notification/getByIdAggregated/getByIdAggregated.ts deleted file mode 100644 index 1cf39382..00000000 --- a/src/domain/notification/getByIdAggregated/getByIdAggregated.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import type { Requester } from '^/domain/authentication'; - -import type { AggregatedData } from '../aggregate'; -import aggregate from '../aggregate'; -import getById from '../getById'; - -export default async function getByIdAggregated(requester: Requester, id: string): Promise -{ - const data = await getById(id); - - return aggregate(requester, data); -} diff --git a/src/domain/notification/getByIdAggregated/index.ts b/src/domain/notification/getByIdAggregated/index.ts deleted file mode 100644 index 8f7f2b94..00000000 --- a/src/domain/notification/getByIdAggregated/index.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export { default } from './getByIdAggregated'; diff --git a/src/domain/notification/getRecentAggregated/getRecentAggregated.ts b/src/domain/notification/getRecentAggregated/getRecentAggregated.ts index 0d9384f3..e7dbb559 100644 --- a/src/domain/notification/getRecentAggregated/getRecentAggregated.ts +++ b/src/domain/notification/getRecentAggregated/getRecentAggregated.ts @@ -3,18 +3,19 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getRecent from '../getRecent'; -export default async function getRecentAggregated(requester: Requester, range: Range): Promise +export default async function getRecentAggregated(tenant: Tenant, requester: Requester, range: Range): Promise { validateRange(range); const data = await getRecent(requester.id, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/notification/notify/createdPost.ts b/src/domain/notification/notify/createdPost.ts index 70036001..476c3bc3 100644 --- a/src/domain/notification/notify/createdPost.ts +++ b/src/domain/notification/notify/createdPost.ts @@ -4,14 +4,14 @@ import getPost from '^/domain/post/getById'; import create from '../create'; import { Types } from '../definitions'; -export default async function createdPost(creatorId: string, postId: string, parentId?: string): Promise +export default async function createdPost(tenantId: string, creatorId: string, postId: string, parentId?: string): Promise { if (parentId === undefined) { return; } - const parentPost = await getPost(parentId); + const parentPost = await getPost(tenantId, parentId); return create(Types.REACTED_TO_POST, creatorId, parentPost.creatorId, postId); } diff --git a/src/domain/notification/notify/ratedPost.ts b/src/domain/notification/notify/ratedPost.ts index 8c008037..e787fcd6 100644 --- a/src/domain/notification/notify/ratedPost.ts +++ b/src/domain/notification/notify/ratedPost.ts @@ -4,14 +4,14 @@ import getPost from '^/domain/post/getById'; import create from '../create'; import { Types } from '../definitions'; -export default async function ratedPost(creatorId: string, postId: string, rated: boolean): Promise +export default async function ratedPost(tenantId: string, creatorId: string, postId: string, rated: boolean): Promise { if (rated === false) { return; } - const post = await getPost(postId); + const post = await getPost(tenantId, postId); return create(Types.RATED_POST, creatorId, post.creatorId, postId); } diff --git a/src/domain/notification/notify/subscriptions.ts b/src/domain/notification/notify/subscriptions.ts index 884fc682..66c87f36 100644 --- a/src/domain/notification/notify/subscriptions.ts +++ b/src/domain/notification/notify/subscriptions.ts @@ -12,8 +12,8 @@ import startedFollowing from './startedFollowing'; async function subscribe(): Promise { await Promise.all([ - subscribeToPostRated(({ creatorId, postId, rated }) => ratedPost(creatorId, postId, rated)), - subscribeToPostCreated(({ creatorId, postId, parentId }) => reactedToPost(creatorId, postId, parentId)), + subscribeToPostRated(({ tenantId, creatorId, postId, rated }) => ratedPost(tenantId, creatorId, postId, rated)), + subscribeToPostCreated(({ tenantId, creatorId, postId, parentId }) => reactedToPost(tenantId, creatorId, postId, parentId)), subscribeToRelationEstablished(({ followerId, followingId }) => startedFollowing(followerId, followingId)), subscribeToPostRemoved(({ postId }) => removedPost(postId)), ]); diff --git a/src/domain/post/aggregate/aggregate.ts b/src/domain/post/aggregate/aggregate.ts index 67b7104b..473c28da 100644 --- a/src/domain/post/aggregate/aggregate.ts +++ b/src/domain/post/aggregate/aggregate.ts @@ -5,14 +5,15 @@ import getCommentData from '^/domain/comment/getById'; import getMetrics from '^/domain/post.metrics/getByPost'; import ratingExists from '^/domain/rating/exists'; import getRelationData from '^/domain/relation/getAggregated'; +import type { Tenant } from '^/domain/tenant'; import type { DataModel } from '../types'; import type { AggregatedData } from './types'; -export default async function aggregate(requester: Requester, data: DataModel): Promise +export default async function aggregate(tenant: Tenant, requester: Requester, data: DataModel): Promise { const [creatorData, isRated, comicData, commentData, metricsData] = await Promise.all([ - getRelationData(requester.id, data.creatorId), + getRelationData(tenant, requester, requester.id, data.creatorId), ratingExists(requester.id, data.id), data.comicId ? getComicData(data.comicId) : Promise.resolve(undefined), data.commentId ? getCommentData(data.commentId) : Promise.resolve(undefined), diff --git a/src/domain/post/create/InvalidPost.ts b/src/domain/post/create/InvalidPost.ts index 02c1caaa..f130de73 100644 --- a/src/domain/post/create/InvalidPost.ts +++ b/src/domain/post/create/InvalidPost.ts @@ -1,7 +1,7 @@ import { ValidationError } from '^/integrations/runtime'; -export default class InvalidReaction extends ValidationError +export default class InvalidPost extends ValidationError { } diff --git a/src/domain/post/create/create.ts b/src/domain/post/create/create.ts index 9eef3339..0fde64b3 100644 --- a/src/domain/post/create/create.ts +++ b/src/domain/post/create/create.ts @@ -7,19 +7,19 @@ import insertData from './insertData'; import publish from './publish'; import validateData from './validateData'; -export default async function create(creatorId: string, comicId?: string, commentId?: string, parentId?: string): Promise +export default async function create(tenantId: string, creatorId: string, comicId?: string, commentId?: string, parentId?: string): Promise { let postId; try { - const data = createData(creatorId, comicId, commentId, parentId); + const data = createData(tenantId, creatorId, comicId, commentId, parentId); validateData(data); postId = await insertData(data); - await publish(creatorId, postId, parentId); + await publish(tenantId, creatorId, postId, parentId); return postId; } diff --git a/src/domain/post/create/createData.ts b/src/domain/post/create/createData.ts index 2a61129d..91ade3f8 100644 --- a/src/domain/post/create/createData.ts +++ b/src/domain/post/create/createData.ts @@ -3,14 +3,15 @@ import { generateId } from '^/integrations/utilities/crypto'; import type { DataModel } from '../types'; -export default function createData(creatorId: string, comicId?: string, commentId?: string, parentId?: string): DataModel +export default function createData(tenantId: string, creatorId: string, comicId?: string, commentId?: string, parentId?: string): DataModel { return { id: generateId(), - parentId, + tenantId, creatorId, comicId, commentId, + parentId, createdAt: new Date().toISOString() }; } diff --git a/src/domain/post/create/publish.ts b/src/domain/post/create/publish.ts index a9eefc41..86883d9a 100644 --- a/src/domain/post/create/publish.ts +++ b/src/domain/post/create/publish.ts @@ -5,12 +5,12 @@ import { EVENT_CHANNEL } from '../definitions'; import { EVENT_NAME } from './definitions'; import type { CreatedPublication } from './types'; -export default async function publish(creatorId: string, postId: string, parentId?: string): Promise +export default async function publish(tenantId: string, creatorId: string, postId: string, parentId?: string): Promise { const publication: CreatedPublication = { channel: EVENT_CHANNEL, name: EVENT_NAME, - data: { creatorId, postId, parentId } + data: { tenantId, creatorId, postId, parentId } }; return eventBroker.publish(publication); diff --git a/src/domain/post/create/types.ts b/src/domain/post/create/types.ts index d8dba069..7de8b5d6 100644 --- a/src/domain/post/create/types.ts +++ b/src/domain/post/create/types.ts @@ -3,9 +3,10 @@ import type { Publication, Subscription } from '^/integrations/eventbroker'; import type { DataModel } from '../types'; -export type ValidationModel = Pick; +export type ValidationModel = Pick; export type CreatedEventData = { + tenantId: string; creatorId: string; postId: string; parentId?: string; diff --git a/src/domain/post/create/validateData.ts b/src/domain/post/create/validateData.ts index 9ae6e3d8..3d8fce64 100644 --- a/src/domain/post/create/validateData.ts +++ b/src/domain/post/create/validateData.ts @@ -9,13 +9,14 @@ import type { ValidationModel } from './types'; const schema: ValidationSchema = { + tenantId: requiredIdValidation, creatorId: requiredIdValidation, comicId: optionalIdValidation, commentId: optionalIdValidation, parentId: optionalIdValidation }; -export default function validateData({ creatorId, comicId, commentId, parentId }: ValidationModel): void +export default function validateData({ tenantId, creatorId, comicId, commentId, parentId }: ValidationModel): void { if (comicId === undefined && commentId === undefined) { @@ -26,7 +27,7 @@ export default function validateData({ creatorId, comicId, commentId, parentId } throw new InvalidPost(messages); } - const result = validator.validate({ creatorId, comicId, commentId, parentId }, schema); + const result = validator.validate({ tenantId, creatorId, comicId, commentId, parentId }, schema); if (result.invalid) { diff --git a/src/domain/post/createWithComic/createWithComic.ts b/src/domain/post/createWithComic/createWithComic.ts index 53cf9f11..32b01d8f 100644 --- a/src/domain/post/createWithComic/createWithComic.ts +++ b/src/domain/post/createWithComic/createWithComic.ts @@ -1,12 +1,13 @@ import type { Requester } from '^/domain/authentication'; import createComic from '^/domain/comic/create'; +import type { Tenant } from '^/domain/tenant'; import createPost from '../create'; -export default async function createWithComic(requester: Requester, comicImageDataUrl: string, parentId: string | undefined = undefined): Promise +export default async function createWithComic(tenant: Tenant, requester: Requester, comicImageDataUrl: string, parentId: string | undefined = undefined): Promise { const comicId = await createComic(comicImageDataUrl); - return createPost(requester.id, comicId, undefined, parentId); + return createPost(tenant.id, requester.id, comicId, undefined, parentId); } diff --git a/src/domain/post/createWithComment/createWithComment.ts b/src/domain/post/createWithComment/createWithComment.ts index 50b50a0b..6d36295e 100644 --- a/src/domain/post/createWithComment/createWithComment.ts +++ b/src/domain/post/createWithComment/createWithComment.ts @@ -1,12 +1,13 @@ import type { Requester } from '^/domain/authentication'; import createComment from '^/domain/comment/create'; +import type { Tenant } from '^/domain/tenant'; import createPost from '../create'; -export default async function createWithComment(requester: Requester, message: string, parentId: string | undefined = undefined): Promise +export default async function createWithComment(tenant: Tenant, requester: Requester, message: string, parentId: string | undefined = undefined): Promise { const commentId = await createComment(message); - return createPost(requester.id, undefined, commentId, parentId); + return createPost(tenant.id, requester.id, undefined, commentId, parentId); } diff --git a/src/domain/post/explore/explore.ts b/src/domain/post/explore/explore.ts index 7d6e0cc4..b08cb69e 100644 --- a/src/domain/post/explore/explore.ts +++ b/src/domain/post/explore/explore.ts @@ -1,16 +1,17 @@ import type { Requester } from '^/domain/authentication'; import retrieveRelationsByFollower from '^/domain/relation/getFollowing'; +import type { Tenant } from '^/domain/tenant'; import type { DataModel } from '../types'; import retrieveData from './retrieveData'; -export default async function explore(requester: Requester, limit: number, offset: number): Promise +export default async function explore(tenant: Tenant, requester: Requester, limit: number, offset: number): Promise { const relationsData = await retrieveRelationsByFollower(requester, requester.id); const excludedCreatorIds = relationsData.map(data => data.followingId); excludedCreatorIds.push(requester.id); - return retrieveData(excludedCreatorIds, limit, offset); + return retrieveData(tenant.id, excludedCreatorIds, limit, offset); } diff --git a/src/domain/post/explore/retrieveData.ts b/src/domain/post/explore/retrieveData.ts index a3178c6d..db243979 100644 --- a/src/domain/post/explore/retrieveData.ts +++ b/src/domain/post/explore/retrieveData.ts @@ -1,17 +1,18 @@ -import type { RecordQuery, RecordSort} from '^/integrations/database'; +import type { RecordQuery, RecordSort } from '^/integrations/database'; import database, { SortDirections } from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function retrieveData(excludedCreatorIds: string[], limit: number, offset: number): Promise +export default async function retrieveData(tenantId: string, excludedCreatorIds: string[], limit: number, offset: number): Promise { const query: RecordQuery = { - deleted: { 'EQUALS': false }, - parentId: { 'EQUALS': undefined }, - creatorId: { NOT_IN: excludedCreatorIds } + tenantId: { EQUALS: tenantId }, + creatorId: { NOT_IN: excludedCreatorIds }, + parentId: { EQUALS: undefined }, + deleted: { EQUALS: false }, }; const sort: RecordSort = { createdAt: SortDirections.DESCENDING }; diff --git a/src/domain/post/exploreAggregated/exploreAggregated.ts b/src/domain/post/exploreAggregated/exploreAggregated.ts index 2c22398a..27e7a8d9 100644 --- a/src/domain/post/exploreAggregated/exploreAggregated.ts +++ b/src/domain/post/exploreAggregated/exploreAggregated.ts @@ -3,18 +3,19 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import explore from '../explore'; -export default async function exploreAggregated(requester: Requester, range: Range): Promise +export default async function exploreAggregated(tenant: Tenant, requester: Requester, range: Range): Promise { validateRange(range); - const data = await explore(requester, range.limit, range.offset); + const data = await explore(tenant, requester, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/post/getByCreatorAggregated/getByCreatorAggregated.ts b/src/domain/post/getByCreatorAggregated/getByCreatorAggregated.ts index 92557fbd..312f1d1b 100644 --- a/src/domain/post/getByCreatorAggregated/getByCreatorAggregated.ts +++ b/src/domain/post/getByCreatorAggregated/getByCreatorAggregated.ts @@ -3,6 +3,7 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; @@ -10,13 +11,13 @@ import getByCreator from '../getByCreator'; export { type AggregatedData }; -export default async function getByCreatorAggregated(requester: Requester, creatorId: string, range: Range): Promise +export default async function getByCreatorAggregated(tenant: Tenant, requester: Requester, creatorId: string, range: Range): Promise { validateRange(range); const data = await getByCreator(creatorId, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/post/getByFollowingAggregated/getByFollowingAggregated.ts b/src/domain/post/getByFollowingAggregated/getByFollowingAggregated.ts index 9de65dd7..108d4c0c 100644 --- a/src/domain/post/getByFollowingAggregated/getByFollowingAggregated.ts +++ b/src/domain/post/getByFollowingAggregated/getByFollowingAggregated.ts @@ -3,18 +3,19 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getByFollowing from '../getByFollowing'; -export default async function getByFollowingAggregated(requester: Requester, range: Range): Promise +export default async function getByFollowingAggregated(tenant: Tenant, requester: Requester, range: Range): Promise { validateRange(range); const data = await getByFollowing(requester, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/post/getById/getById.ts b/src/domain/post/getById/getById.ts index 9a8f3288..360439a0 100644 --- a/src/domain/post/getById/getById.ts +++ b/src/domain/post/getById/getById.ts @@ -6,12 +6,13 @@ import { RECORD_TYPE } from '../definitions'; import PostNotFound from '../PostNotFound'; import type { DataModel } from '../types'; -export default async function getById(id: string): Promise +export default async function getById(tenantId: string, id: string): Promise { const query: RecordQuery = { - id: { 'EQUALS': id }, - deleted: { 'EQUALS': false } + tenantId: { EQUALS: tenantId }, + id: { EQUALS: id }, + deleted: { EQUALS: false } }; const record = await database.findRecord(RECORD_TYPE, query); diff --git a/src/domain/post/getByIdAggregated/getByIdAggregated.ts b/src/domain/post/getByIdAggregated/getByIdAggregated.ts index f3583dcb..7b269a56 100644 --- a/src/domain/post/getByIdAggregated/getByIdAggregated.ts +++ b/src/domain/post/getByIdAggregated/getByIdAggregated.ts @@ -1,5 +1,6 @@ import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; @@ -7,9 +8,9 @@ import getById from '../getById'; export { type AggregatedData }; -export default async function getByIdAggregated(requester: Requester, id: string): Promise +export default async function getByIdAggregated(tenant: Tenant, requester: Requester, id: string): Promise { - const data = await getById(id); + const data = await getById(tenant.id, id); - return aggregate(requester, data); + return aggregate(tenant, requester, data); } diff --git a/src/domain/post/getByParent/getByParent.ts b/src/domain/post/getByParent/getByParent.ts index 600083ff..1e46e1ba 100644 --- a/src/domain/post/getByParent/getByParent.ts +++ b/src/domain/post/getByParent/getByParent.ts @@ -1,16 +1,17 @@ -import type { RecordQuery, RecordSort} from '^/integrations/database'; +import type { RecordQuery, RecordSort } from '^/integrations/database'; import database, { SortDirections } from '^/integrations/database'; import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function getByParent(parentId: string, limit: number, offset: number): Promise +export default async function getByParent(tenantId: string, parentId: string, limit: number, offset: number): Promise { const query: RecordQuery = { - parentId: { 'EQUALS': parentId }, - deleted: { 'EQUALS': false } + tenantId: { EQUALS: tenantId }, + parentId: { EQUALS: parentId }, + deleted: { EQUALS: false } }; const sort: RecordSort = { createdAt: SortDirections.DESCENDING }; diff --git a/src/domain/post/getByParentAggregated/getByParentAggregated.ts b/src/domain/post/getByParentAggregated/getByParentAggregated.ts index 55d3783e..f68457d1 100644 --- a/src/domain/post/getByParentAggregated/getByParentAggregated.ts +++ b/src/domain/post/getByParentAggregated/getByParentAggregated.ts @@ -2,16 +2,17 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getByParent from '../getByParent'; -export default async function getByParentAggregated(requester: Requester, postId: string, range: Range): Promise +export default async function getByParentAggregated(tenant: Tenant, requester: Requester, postId: string, range: Range): Promise { - const data = await getByParent(postId, range.limit, range.offset); + const data = await getByParent(tenant.id, postId, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/post/getRecommended/getRecommended.ts b/src/domain/post/getRecommended/getRecommended.ts index 0beb51c1..93dbcd9c 100644 --- a/src/domain/post/getRecommended/getRecommended.ts +++ b/src/domain/post/getRecommended/getRecommended.ts @@ -2,18 +2,17 @@ import type { RecordQuery, RecordSort } from '^/integrations/database'; import database, { SortDirections } from '^/integrations/database'; -import type { Requester } from '^/domain/authentication'; - import { RECORD_TYPE } from '../definitions'; import type { DataModel } from '../types'; -export default async function getRecommended(requester: Requester, limit: number, offset: number): Promise +export default async function getRecommended(tenantId: string, requesterId: string, limit: number, offset: number): Promise { const query: RecordQuery = { - deleted: { EQUALS: false }, + tenantId: { EQUALS: tenantId }, + creatorId: { NOT_EQUALS: requesterId }, parentId: { EQUALS: undefined }, - creatorId: { NOT_EQUALS: requester.id } + deleted: { EQUALS: false } }; const sort: RecordSort = { createdAt: SortDirections.DESCENDING }; diff --git a/src/domain/post/getRecommendedAggregated/getRecommendedAggregated.ts b/src/domain/post/getRecommendedAggregated/getRecommendedAggregated.ts index bd820212..23be9c13 100644 --- a/src/domain/post/getRecommendedAggregated/getRecommendedAggregated.ts +++ b/src/domain/post/getRecommendedAggregated/getRecommendedAggregated.ts @@ -3,18 +3,19 @@ import type { Requester } from '^/domain/authentication'; import filterResolved from '^/domain/common/filterResolved'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getRecommended from '../getRecommended'; -export default async function getRecommendedAggregated(requester: Requester, range: Range): Promise +export default async function getRecommendedAggregated(tenant: Tenant, requester: Requester, range: Range): Promise { validateRange(range); - const data = await getRecommended(requester, range.limit, range.offset); + const data = await getRecommended(tenant.id, requester.id, range.limit, range.offset); - const aggregates = data.map(item => aggregate(requester, item)); + const aggregates = data.map(item => aggregate(tenant, requester, item)); return filterResolved(aggregates); } diff --git a/src/domain/post/remove/remove.ts b/src/domain/post/remove/remove.ts index 278631c6..86a850d0 100644 --- a/src/domain/post/remove/remove.ts +++ b/src/domain/post/remove/remove.ts @@ -2,6 +2,7 @@ import logger from '^/integrations/logging'; import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import getById from '../getById'; import PostNotFound from '../PostNotFound'; @@ -10,7 +11,7 @@ import isNotOwner from './isNotOwner'; import publish from './publish'; import undeleteData from './undeleteData'; -export default async function remove(requester: Requester, id: string): Promise +export default async function remove(tenant: Tenant, requester: Requester, id: string): Promise { // We only delete the post itself and do not cascade it towards it's children as it doesn't add // any value, and it would make the code more complex. @@ -19,7 +20,7 @@ export default async function remove(requester: Requester, id: string): Promise< try { - const post = await getById(id); + const post = await getById(tenant.id, id); if (isNotOwner(post, requester.id)) { diff --git a/src/domain/post/types.ts b/src/domain/post/types.ts index a948aafc..00104857 100644 --- a/src/domain/post/types.ts +++ b/src/domain/post/types.ts @@ -3,6 +3,7 @@ import type { BaseDataModel, CountOperation } from '../types'; type DataModel = BaseDataModel & { + readonly tenantId: string; readonly id: string; readonly creatorId: string; readonly comicId?: string; diff --git a/src/domain/rating/toggle/publish.ts b/src/domain/rating/toggle/publish.ts index edd6adb5..0869bc6b 100644 --- a/src/domain/rating/toggle/publish.ts +++ b/src/domain/rating/toggle/publish.ts @@ -5,12 +5,12 @@ import { EVENT_CHANNEL } from '../definitions'; import { EVENT_NAME } from './definitions'; import type { ToggledPublication } from './types'; -export default async function publish(creatorId: string, postId: string, rated: boolean): Promise +export default async function publish(tenantId: string, creatorId: string, postId: string, rated: boolean): Promise { const publication: ToggledPublication = { channel: EVENT_CHANNEL, name: EVENT_NAME, - data: { creatorId, postId, rated } + data: { tenantId, creatorId, postId, rated } }; return eventBroker.publish(publication); diff --git a/src/domain/rating/toggle/switchOff.ts b/src/domain/rating/toggle/switchOff.ts index fb7ef1cb..d82acb7b 100644 --- a/src/domain/rating/toggle/switchOff.ts +++ b/src/domain/rating/toggle/switchOff.ts @@ -4,13 +4,13 @@ import erase from '../erase'; import type { DataModel } from '../types'; import publish from './publish'; -export default async function switchOff(rating: DataModel): Promise +export default async function switchOff(tenantId: string, rating: DataModel): Promise { await erase(rating.id); try { - await publish(rating.creatorId, rating.postId, false); + await publish(tenantId, rating.creatorId, rating.postId, false); return false; } diff --git a/src/domain/rating/toggle/switchOn.ts b/src/domain/rating/toggle/switchOn.ts index f04942a0..5819f5be 100644 --- a/src/domain/rating/toggle/switchOn.ts +++ b/src/domain/rating/toggle/switchOn.ts @@ -3,13 +3,13 @@ import create from '../create'; import erase from '../erase'; import publish from './publish'; -export default async function switchOn(creatorId: string, postId: string): Promise +export default async function switchOn(tenantId: string, creatorId: string, postId: string): Promise { const id = await create(creatorId, postId); try { - await publish(creatorId, postId, true); + await publish(tenantId, creatorId, postId, true); return true; } diff --git a/src/domain/rating/toggle/toggle.ts b/src/domain/rating/toggle/toggle.ts index 9e3f7709..9b9d023c 100644 --- a/src/domain/rating/toggle/toggle.ts +++ b/src/domain/rating/toggle/toggle.ts @@ -1,15 +1,16 @@ import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; import getData from './getData'; import switchOff from './switchOff'; import switchOn from './switchOn'; -export default async function toggle(requester: Requester, postId: string): Promise +export default async function toggle(tenant: Tenant, requester: Requester, postId: string): Promise { const data = await getData(requester.id, postId); return data === undefined - ? switchOn(requester.id, postId) - : switchOff(data); + ? switchOn(tenant.id, requester.id, postId) + : switchOff(tenant.id, data); } diff --git a/src/domain/rating/toggle/types.ts b/src/domain/rating/toggle/types.ts index b80162af..ae7673c9 100644 --- a/src/domain/rating/toggle/types.ts +++ b/src/domain/rating/toggle/types.ts @@ -2,6 +2,7 @@ import type { Publication, Subscription } from '^/integrations/eventbroker'; export type ToggledEventData = { + tenantId: string; creatorId: string; postId: string; rated: boolean; diff --git a/src/domain/relation/aggregate/aggregate.ts b/src/domain/relation/aggregate/aggregate.ts index a9db8a23..fbfc8624 100644 --- a/src/domain/relation/aggregate/aggregate.ts +++ b/src/domain/relation/aggregate/aggregate.ts @@ -1,12 +1,13 @@ import getCreatorData from '^/domain/creator/getByIdAggregated'; +import type { Tenant } from '^/domain/tenant'; import type { DataModel } from '../types'; import type { AggregatedData } from './types'; -export default async function aggregate(data: DataModel): Promise +export default async function aggregate(tenant: Tenant, data: DataModel): Promise { - const followingData = await getCreatorData(data.followingId); + const followingData = await getCreatorData(tenant.id, data.followingId); return { id: data.id, diff --git a/src/domain/relation/establish/establish.ts b/src/domain/relation/establish/establish.ts index 5716e0d6..68a68da5 100644 --- a/src/domain/relation/establish/establish.ts +++ b/src/domain/relation/establish/establish.ts @@ -2,6 +2,8 @@ import logger from '^/integrations/logging'; import type { Requester } from '^/domain/authentication'; +import getCreator from '^/domain/creator/getById'; +import type { Tenant } from '^/domain/tenant'; import create from '../create'; import erase from '../erase'; @@ -9,12 +11,14 @@ import exists from '../exists'; import publish from './publish'; import RelationAlreadyExists from './RelationAlreadyExists'; -export default async function establish(requester: Requester, followingId: string): Promise +export default async function establish(tenant: Tenant, requester: Requester, followingId: string): Promise { let id; try { + await getCreator(tenant.id, followingId); + const relationExists = await exists(requester.id, followingId); if (relationExists) diff --git a/src/domain/relation/explore/explore.ts b/src/domain/relation/explore/explore.ts index 5004b639..520de360 100644 --- a/src/domain/relation/explore/explore.ts +++ b/src/domain/relation/explore/explore.ts @@ -1,18 +1,19 @@ import type { Requester } from '^/domain/authentication'; import getOtherCreators from '^/domain/creator/getOthers'; +import type { Tenant } from '^/domain/tenant'; import type { SortOrder } from '../definitions'; import getFollowing from '../getFollowing'; import type { DataModel } from '../types'; -export default async function explore(requester: Requester, order: SortOrder, limit: number, offset: number, search: string | undefined = undefined): Promise +export default async function explore(tenant: Tenant, requester: Requester, order: SortOrder, limit: number, offset: number, search: string | undefined = undefined): Promise { const followingData = await getFollowing(requester, requester.id); const followingIds = followingData.map(data => data.followingId); followingIds.push(requester.id); - const creatorData = await getOtherCreators(followingIds, order, limit, offset, search); + const creatorData = await getOtherCreators(tenant.id, followingIds, order, limit, offset, search); return creatorData.map(data => { diff --git a/src/domain/relation/exploreAggregated/exploreAggregated.ts b/src/domain/relation/exploreAggregated/exploreAggregated.ts index f421b2e1..d6d9270e 100644 --- a/src/domain/relation/exploreAggregated/exploreAggregated.ts +++ b/src/domain/relation/exploreAggregated/exploreAggregated.ts @@ -2,17 +2,18 @@ import type { Requester } from '^/domain/authentication'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import type { SortOrder } from '../definitions'; import explore from '../explore'; -export default async function exploreAggregated(requester: Requester, order: SortOrder, range: Range, search: string | undefined = undefined): Promise +export default async function exploreAggregated(tenant: Tenant, requester: Requester, order: SortOrder, range: Range, search: string | undefined = undefined): Promise { validateRange(range); - const data = await explore(requester, order, range.limit, range.offset, search); + const data = await explore(tenant, requester, order, range.limit, range.offset, search); - return Promise.all(data.map(item => aggregate(item))); + return Promise.all(data.map(item => aggregate(tenant, item))); } diff --git a/src/domain/relation/getAggregated/getAggregated.ts b/src/domain/relation/getAggregated/getAggregated.ts index 56989265..5af6c16a 100644 --- a/src/domain/relation/getAggregated/getAggregated.ts +++ b/src/domain/relation/getAggregated/getAggregated.ts @@ -1,11 +1,14 @@ +import type { Requester } from '^/domain/authentication'; +import type { Tenant } from '^/domain/tenant'; + import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import get from '../get'; -export default async function getAggregated(followerId: string, followingId: string): Promise +export default async function getAggregated(tenant: Tenant, requester: Requester, followerId: string, followingId: string): Promise { const data = await get(followerId, followingId); - return aggregate(data); + return aggregate(tenant, data); } diff --git a/src/domain/relation/getFollowersAggregated/getFollowersAggregated.ts b/src/domain/relation/getFollowersAggregated/getFollowersAggregated.ts index 88ba8558..03017f8a 100644 --- a/src/domain/relation/getFollowersAggregated/getFollowersAggregated.ts +++ b/src/domain/relation/getFollowersAggregated/getFollowersAggregated.ts @@ -2,16 +2,17 @@ import type { Requester } from '^/domain/authentication'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import getFollowers from '../getFollowers'; -export default async function getFollowersAggregated(requester: Requester, followingId: string, range: Range): Promise +export default async function getFollowersAggregated(tenant: Tenant, requester: Requester, followingId: string, range: Range): Promise { validateRange(range); const data = await getFollowers(requester, followingId, range.limit, range.offset); - return Promise.all(data.map(aggregate)); + return Promise.all(data.map(data => aggregate(tenant, data))); } diff --git a/src/domain/relation/getFollowingAggregated/getFollowingAggregated.ts b/src/domain/relation/getFollowingAggregated/getFollowingAggregated.ts index e6d921a8..0ec87187 100644 --- a/src/domain/relation/getFollowingAggregated/getFollowingAggregated.ts +++ b/src/domain/relation/getFollowingAggregated/getFollowingAggregated.ts @@ -2,16 +2,17 @@ import type { Requester } from '^/domain/authentication'; import type { Range } from '^/domain/common/validateRange'; import validateRange from '^/domain/common/validateRange'; +import type { Tenant } from '^/domain/tenant'; import type { AggregatedData } from '../aggregate'; import aggregate from '../aggregate'; import retrieveByFollower from '../getFollowing'; -export default async function getFollowingAggregated(requester: Requester, followerId: string, range: Range): Promise +export default async function getFollowingAggregated(tenant: Tenant, requester: Requester, followerId: string, range: Range): Promise { validateRange(range); const data = await retrieveByFollower(requester, followerId, range.limit, range.offset); - return Promise.all(data.map(aggregate)); + return Promise.all(data.map(item => aggregate(tenant, item))); } diff --git a/src/domain/tenant/definitions.ts b/src/domain/tenant/definitions.ts new file mode 100644 index 00000000..3d7c927c --- /dev/null +++ b/src/domain/tenant/definitions.ts @@ -0,0 +1,2 @@ + +export const RECORD_TYPE = 'tenant'; diff --git a/src/domain/tenant/getByOrigin/TenantNotFound.ts b/src/domain/tenant/getByOrigin/TenantNotFound.ts new file mode 100644 index 00000000..25482e1b --- /dev/null +++ b/src/domain/tenant/getByOrigin/TenantNotFound.ts @@ -0,0 +1,10 @@ + +import { NotFound } from '^/integrations/runtime'; + +export default class TenantNotFound extends NotFound +{ + constructor(origin: string) + { + super(`No tenant found for origin: ${origin}`); + } +} diff --git a/src/domain/tenant/getByOrigin/getByOrigin.ts b/src/domain/tenant/getByOrigin/getByOrigin.ts new file mode 100644 index 00000000..ac1e2342 --- /dev/null +++ b/src/domain/tenant/getByOrigin/getByOrigin.ts @@ -0,0 +1,24 @@ + +import database, { type RecordQuery } from '^/integrations/database'; + +import { RECORD_TYPE } from '../definitions'; +import type { DataModel } from '../types'; + +import TenantNotFound from './TenantNotFound'; + +export default async function getByOrigin(origin: string): Promise +{ + const query: RecordQuery = + { + origins: { 'CONTAINS': origin } + }; + + const record = await database.findRecord(RECORD_TYPE, query); + + if (record === undefined) + { + throw new TenantNotFound(origin); + } + + return record as DataModel; +} diff --git a/src/domain/tenant/getByOrigin/index.ts b/src/domain/tenant/getByOrigin/index.ts new file mode 100644 index 00000000..f940e37a --- /dev/null +++ b/src/domain/tenant/getByOrigin/index.ts @@ -0,0 +1,3 @@ + +export { default } from './getByOrigin'; +export { default as TenantNotFound } from './TenantNotFound'; diff --git a/src/domain/tenant/getByOriginConverted/InvalidOrigin.ts b/src/domain/tenant/getByOriginConverted/InvalidOrigin.ts new file mode 100644 index 00000000..87a6a643 --- /dev/null +++ b/src/domain/tenant/getByOriginConverted/InvalidOrigin.ts @@ -0,0 +1,7 @@ + +import { ValidationError } from '^/integrations/runtime'; + +export default class InvalidOrigin extends ValidationError +{ + +} diff --git a/src/domain/tenant/getByOriginConverted/getByOriginConverted.ts b/src/domain/tenant/getByOriginConverted/getByOriginConverted.ts new file mode 100644 index 00000000..7f8cc791 --- /dev/null +++ b/src/domain/tenant/getByOriginConverted/getByOriginConverted.ts @@ -0,0 +1,16 @@ + +import getByOrigin from '../getByOrigin'; +import type { Tenant } from '../types'; +import validateData from './validateData'; + +export default async function getByOriginConverted(origin: string): Promise +{ + validateData({ origin }); + + const tenant = await getByOrigin(origin); + + return { + id: tenant.id, + origin: origin + }; +} diff --git a/src/domain/tenant/getByOriginConverted/index.ts b/src/domain/tenant/getByOriginConverted/index.ts new file mode 100644 index 00000000..db225e15 --- /dev/null +++ b/src/domain/tenant/getByOriginConverted/index.ts @@ -0,0 +1,2 @@ + +export { default } from './getByOriginConverted'; diff --git a/src/domain/tenant/getByOriginConverted/types.ts b/src/domain/tenant/getByOriginConverted/types.ts new file mode 100644 index 00000000..1739df5a --- /dev/null +++ b/src/domain/tenant/getByOriginConverted/types.ts @@ -0,0 +1,4 @@ + +import type { Tenant } from '../types'; + +export type ValidationModel = Pick; diff --git a/src/domain/tenant/getByOriginConverted/validateData.ts b/src/domain/tenant/getByOriginConverted/validateData.ts new file mode 100644 index 00000000..1d3e6582 --- /dev/null +++ b/src/domain/tenant/getByOriginConverted/validateData.ts @@ -0,0 +1,28 @@ + +import type { ValidationSchema } from '^/integrations/validation'; +import validator from '^/integrations/validation'; + +import InvalidOrigin from './InvalidOrigin'; +import type { ValidationModel } from './types'; + +const schema: ValidationSchema = +{ + origin: + { + message: 'Invalid origin', + URL: + { + required: true + } + } +}; + +export default function validateData({ origin }: ValidationModel): void +{ + const result = validator.validate({ origin }, schema); + + if (result.invalid) + { + throw new InvalidOrigin(result.messages); + } +} diff --git a/src/domain/tenant/index.ts b/src/domain/tenant/index.ts new file mode 100644 index 00000000..7504bb46 --- /dev/null +++ b/src/domain/tenant/index.ts @@ -0,0 +1,6 @@ + +export { RECORD_TYPE } from './definitions'; + +export type { DataModel, Tenant } from './types'; + +export { default as tenant } from './tenant'; diff --git a/src/domain/tenant/tenant.ts b/src/domain/tenant/tenant.ts new file mode 100644 index 00000000..664fe246 --- /dev/null +++ b/src/domain/tenant/tenant.ts @@ -0,0 +1,9 @@ + +import type { Tenant } from './types'; + +const tenant: Tenant = { + id: 'default', + origin: 'localhost' +}; + +export default tenant; diff --git a/src/domain/tenant/types.ts b/src/domain/tenant/types.ts new file mode 100644 index 00000000..7f74c576 --- /dev/null +++ b/src/domain/tenant/types.ts @@ -0,0 +1,13 @@ + +import type { BaseDataModel } from '../types'; + +type DataModel = BaseDataModel & { + readonly origins: string[]; +}; + +type Tenant = { + readonly id: string; + readonly origin: string; +}; + +export type { DataModel, Tenant }; diff --git a/src/integrations/authentication/definitions/interfaces.ts b/src/integrations/authentication/definitions/interfaces.ts index ada2bb26..09ce7f99 100644 --- a/src/integrations/authentication/definitions/interfaces.ts +++ b/src/integrations/authentication/definitions/interfaces.ts @@ -9,9 +9,9 @@ export interface IdentityProvider disconnect(): Promise; - getLoginUrl(): Promise; + getLoginUrl(origin: string): Promise; - login(data: Record): Promise; + login(origin: string, data: Record): Promise; refresh(session: Session): Promise; diff --git a/src/integrations/authentication/implementations/openid/OpenID.ts b/src/integrations/authentication/implementations/openid/OpenID.ts index a5399249..a49e80a0 100644 --- a/src/integrations/authentication/implementations/openid/OpenID.ts +++ b/src/integrations/authentication/implementations/openid/OpenID.ts @@ -16,7 +16,7 @@ type OpenIDConfiguration = { issuer: string; clientId: string; clientSecret: string; - redirectUri: string; + redirectPath: string; allowInsecureRequests: boolean; }; @@ -52,9 +52,9 @@ export default class OpenID implements IdentityProvider this.#clientConfiguration = undefined; } - async getLoginUrl(): Promise + async getLoginUrl(origin: string): Promise { - const redirect_uri = this.#providerConfiguration.redirectUri; + const redirect_uri = new URL(this.#providerConfiguration.redirectPath, origin).href; const scope = 'openid profile email'; const code_challenge = await calculatePKCECodeChallenge(this.#codeVerifier); const code_challenge_method = 'S256'; @@ -72,12 +72,16 @@ export default class OpenID implements IdentityProvider return redirectTo.href; } - async login(data: Record): Promise + async login(origin: string, data: Record): Promise { const clientConfiguration = this.#getClientConfiguration(); - const currentUrl = new URL(`${this.#providerConfiguration.redirectUri}?session_state=${data.session_state}&iss=${data.iss}&code=${data.code}`); - const tokens = await authorizationCodeGrant(clientConfiguration, currentUrl, { + const url = new URL(this.#providerConfiguration.redirectPath, origin); + url.searchParams.set('session_state', data.session_state as string); + url.searchParams.set('iss', data.iss as string); + url.searchParams.set('code', data.code as string); + + const tokens = await authorizationCodeGrant(clientConfiguration, url, { pkceCodeVerifier: this.#codeVerifier, idTokenExpected: true }); diff --git a/src/integrations/authentication/implementations/openid/create.ts b/src/integrations/authentication/implementations/openid/create.ts index f742f9ac..5475f9e9 100644 --- a/src/integrations/authentication/implementations/openid/create.ts +++ b/src/integrations/authentication/implementations/openid/create.ts @@ -6,8 +6,8 @@ export default function create(): OpenID const issuer = process.env.OPENID_ISSUER ?? 'undefined'; const clientId = process.env.OPENID_CLIENT_ID ?? 'undefined'; const clientSecret = process.env.OPENID_CLIENT_SECRET ?? 'undefined'; - const redirectUri = process.env.OPENID_REDIRECT_URI ?? 'undefined'; + const redirectPath = process.env.OPENID_REDIRECT_PATH ?? 'undefined'; const allowInsecureRequests = process.env.OPENID_ALLOW_INSECURE_REQUESTS === 'true'; - return new OpenID({ issuer, clientId, clientSecret, redirectUri, allowInsecureRequests }); + return new OpenID({ issuer, clientId, clientSecret, redirectPath, allowInsecureRequests }); } diff --git a/src/integrations/runtime/authenticationMiddleware.ts b/src/integrations/runtime/authenticationMiddleware.ts index b0bec483..db4475e1 100644 --- a/src/integrations/runtime/authenticationMiddleware.ts +++ b/src/integrations/runtime/authenticationMiddleware.ts @@ -1,6 +1,8 @@ import identityProvider from '^/integrations/authentication'; +import { TENANT_BY_ORIGIN_PATH } from '^/domain/definitions'; + import AuthenticationMiddleware from './middlewares/AuthenticationMiddleware'; const authProcedures = { @@ -9,8 +11,8 @@ const authProcedures = { logout: 'domain/authentication/logout' }; -const redirectUrl = process.env.AUTHENTICATION_CLIENT_URI || 'undefined'; +const redirectPath = process.env.AUTHENTICATION_CLIENT_PATH || 'undefined'; -const whiteList: string[] = []; +const whiteList: string[] = [TENANT_BY_ORIGIN_PATH]; -export default new AuthenticationMiddleware(identityProvider, authProcedures, redirectUrl, whiteList); +export default new AuthenticationMiddleware(identityProvider, authProcedures, redirectPath, whiteList); diff --git a/src/integrations/runtime/middlewares/AuthenticationMiddleware.ts b/src/integrations/runtime/middlewares/AuthenticationMiddleware.ts index 8db31c37..487e4b60 100644 --- a/src/integrations/runtime/middlewares/AuthenticationMiddleware.ts +++ b/src/integrations/runtime/middlewares/AuthenticationMiddleware.ts @@ -1,12 +1,10 @@ -import type { Middleware, NextHandler, Request} from 'jitar'; -import { Response } from 'jitar'; +import type { Middleware, NextHandler, Request } from 'jitar'; +import { Response, Unauthorized } from 'jitar'; import type { IdentityProvider, Session } from '^/integrations/authentication'; import { generateKey } from '^/integrations/utilities/crypto'; -import Unauthorized from '../errors/Unauthorized'; - type AuthProcedures = { loginUrl: string; login: string; @@ -23,14 +21,14 @@ export default class AuthenticationMiddleware implements Middleware { readonly #identityProvider: IdentityProvider; readonly #authProcedures: AuthProcedures; - readonly #redirectUrl: string; + readonly #redirectPath: string; readonly #whiteList: string[]; - constructor(identityProvider: IdentityProvider, authProcedures: AuthProcedures, redirectUrl: string, whiteList: string[]) + constructor(identityProvider: IdentityProvider, authProcedures: AuthProcedures, redirectPath: string, whiteList: string[]) { this.#identityProvider = identityProvider; this.#authProcedures = authProcedures; - this.#redirectUrl = redirectUrl; + this.#redirectPath = redirectPath; this.#whiteList = whiteList; } @@ -43,16 +41,18 @@ export default class AuthenticationMiddleware implements Middleware switch (request.fqn) { - case this.#authProcedures.loginUrl: return this.#getLoginUrl(); + case this.#authProcedures.loginUrl: return this.#getLoginUrl(request); case this.#authProcedures.login: return this.#createSession(request, next); case this.#authProcedures.logout: return this.#destroySession(request, next); default: return this.#handleRequest(request, next); } } - async #getLoginUrl(): Promise + async #getLoginUrl(request: Request): Promise { - const url = await this.#identityProvider.getLoginUrl(); + const origin = this.#getOrigin(request); + + const url = await this.#identityProvider.getLoginUrl(origin); return new Response(200, url); } @@ -60,20 +60,28 @@ export default class AuthenticationMiddleware implements Middleware async #createSession(request: Request, next: NextHandler): Promise { const data = Object.fromEntries(request.args); - const session = await this.#identityProvider.login(data); + const origin = this.#getOrigin(request); + const session = await this.#identityProvider.login(origin, data); request.args.clear(); request.setArgument(IDENTITY_PARAMETER, session.identity); const response = await next(); + if (response.status !== 200) + { + await this.#identityProvider.logout(session); + + return response; + } + session.key = generateKey(); session.requester = response.result; sessions.set(session.key, session); this.#setAuthorizationHeader(response, session); - this.#setRedirectHeader(response, session.key); + this.#setRedirectHeader(response, session.key, origin); return response; } @@ -212,8 +220,13 @@ export default class AuthenticationMiddleware implements Middleware response.setHeader('Authorization', `Bearer ${session.key}`); } - #setRedirectHeader(response: Response, key: string): void + #setRedirectHeader(response: Response, key: string, origin: string): void + { + response.setHeader('Location', new URL(`${this.#redirectPath}?key=${key}`, origin).href); + } + + #getOrigin(request: Request): string { - response.setHeader('Location', `${this.#redirectUrl}?key=${key}`); + return request.getHeader('origin') as string; } } diff --git a/src/integrations/runtime/middlewares/OriginMiddleware.ts b/src/integrations/runtime/middlewares/OriginMiddleware.ts new file mode 100644 index 00000000..6ecba918 --- /dev/null +++ b/src/integrations/runtime/middlewares/OriginMiddleware.ts @@ -0,0 +1,90 @@ + +import type { Middleware, NextHandler, Request, Response } from 'jitar'; +import { BadRequest } from 'jitar'; + +import type { ValidationSchema } from '^/integrations/validation'; +import validator from '^/integrations/validation'; + +const ORIGIN_COOKIE_NAME = 'x-client-origin'; +const schema: ValidationSchema = +{ + origin: + { + message: 'Invalid origin', + URL: + { + required: true + } + } +}; + +export default class OriginMiddleware implements Middleware +{ + async handle(request: Request, next: NextHandler): Promise + { + let fromCookie = true; + + let origin = this.#getOriginFromCookie(request); + + if (origin === undefined) + { + fromCookie = false; + + origin = this.#getOriginFromHeader(request); + } + + this.#validateOriginValue(origin); + + // The origin header is validated and set here for use in other middlewares + request.setHeader('origin', origin!); + + const response = await next(); + + if (fromCookie === false) + { + this.#setOriginCookie(response, origin as string); + } + + return response; + } + + #getOriginFromHeader(request: Request): string | undefined + { + return request.getHeader('origin'); + } + + #getOriginFromCookie(request: Request): string | undefined + { + const header = request.getHeader('cookie'); + + if (header === undefined) + { + return; + } + + for (const cookie of header.split(';')) + { + const [key, value] = cookie.split('='); + + if (key.trim() === ORIGIN_COOKIE_NAME) + { + return value?.trim(); + } + } + } + + #validateOriginValue(value: string | undefined): void + { + const result = validator.validate({ origin: value }, schema); + + if (result.invalid) + { + throw new BadRequest('Invalid origin'); + } + } + + #setOriginCookie(response: Response, origin: string): void + { + response.setHeader('Set-Cookie', `${ORIGIN_COOKIE_NAME}=${origin}; Path=/; HttpOnly=true; SameSite=None; Secure`); + } +} diff --git a/src/integrations/runtime/middlewares/TenantMiddleware.ts b/src/integrations/runtime/middlewares/TenantMiddleware.ts new file mode 100644 index 00000000..6fa5e36d --- /dev/null +++ b/src/integrations/runtime/middlewares/TenantMiddleware.ts @@ -0,0 +1,62 @@ + +import type { Middleware, NextHandler, Request, Response } from 'jitar'; + +const TENANT_PARAMETER = '*tenant'; + +export default class TenantMiddleware implements Middleware +{ + readonly #cache = new Map(); + readonly #getTenantPath: string; + + constructor(tenantPath: string) + { + this.#getTenantPath = tenantPath; + } + + async handle(request: Request, next: NextHandler): Promise + { + return request.fqn === this.#getTenantPath + ? this.#getTenant(request, next) + : this.#handleRequest(request, next); + } + + async #getTenant(request: Request, next: NextHandler): Promise + { + const origin = this.#getOrigin(request); + const cached = this.#cache.get(origin); + + if (cached === undefined) + { + request.setArgument('origin', origin); + + const response = await next(); + + if (response.status === 200) + { + this.#cache.set(origin, response); + } + + return response; + } + + return cached; + } + + async #handleRequest(request: Request, next: NextHandler): Promise + { + const origin = this.#getOrigin(request); + const cached = this.#cache.get(origin); + + if (cached !== undefined) + { + request.setArgument(TENANT_PARAMETER, cached.result); + } + + return next(); + } + + #getOrigin(request: Request): string + { + return request.getHeader('origin') as string; + } +} diff --git a/src/integrations/runtime/originMiddleware.ts b/src/integrations/runtime/originMiddleware.ts new file mode 100644 index 00000000..a6f56685 --- /dev/null +++ b/src/integrations/runtime/originMiddleware.ts @@ -0,0 +1,4 @@ + +import OriginMiddleware from './middlewares/OriginMiddleware'; + +export default new OriginMiddleware(); diff --git a/src/integrations/runtime/setUpBff.ts b/src/integrations/runtime/setUpBff.ts index 5c1f24ff..1a917ae8 100644 --- a/src/integrations/runtime/setUpBff.ts +++ b/src/integrations/runtime/setUpBff.ts @@ -1,10 +1,12 @@ +import identityProvider from '^/integrations/authentication'; import eventBroker from '^/integrations/eventbroker'; try { await Promise.allSettled([ - eventBroker.connect() + eventBroker.connect(), + identityProvider.connect() ]); } catch (error) @@ -12,6 +14,7 @@ catch (error) const disconnections = []; if (eventBroker.connected) disconnections.push(eventBroker.disconnect()); + if (identityProvider.connected) disconnections.push(identityProvider.disconnect()); await Promise.allSettled(disconnections); diff --git a/src/integrations/runtime/setUpGateway.ts b/src/integrations/runtime/setUpGateway.ts deleted file mode 100644 index 3f639155..00000000 --- a/src/integrations/runtime/setUpGateway.ts +++ /dev/null @@ -1,4 +0,0 @@ - -import identityProvider from '^/integrations/authentication'; - -await identityProvider.connect(); diff --git a/src/integrations/runtime/tearDownBff.ts b/src/integrations/runtime/tearDownBff.ts index 4aa38270..93f3a758 100644 --- a/src/integrations/runtime/tearDownBff.ts +++ b/src/integrations/runtime/tearDownBff.ts @@ -1,8 +1,10 @@ +import identityProvider from '^/integrations/authentication'; import eventBroker from '^/integrations/eventbroker'; const disconnections = []; if (eventBroker.connected) disconnections.push(eventBroker.disconnect()); +if (identityProvider.connected) disconnections.push(identityProvider.disconnect()); await Promise.allSettled(disconnections); diff --git a/src/integrations/runtime/tearDownGateway.ts b/src/integrations/runtime/tearDownGateway.ts deleted file mode 100644 index f4c67229..00000000 --- a/src/integrations/runtime/tearDownGateway.ts +++ /dev/null @@ -1,4 +0,0 @@ - -import identityProvider from '^/integrations/authentication'; - -if (identityProvider.connected) await identityProvider.disconnect(); diff --git a/src/integrations/runtime/tenantMiddleware.ts b/src/integrations/runtime/tenantMiddleware.ts new file mode 100644 index 00000000..3d4bc2c8 --- /dev/null +++ b/src/integrations/runtime/tenantMiddleware.ts @@ -0,0 +1,8 @@ + +import { TENANT_BY_ORIGIN_PATH } from '^/domain/definitions'; + +import TenantMiddleware from './middlewares/TenantMiddleware'; + +const tenantPath = TENANT_BY_ORIGIN_PATH; + +export default new TenantMiddleware(tenantPath); diff --git a/src/webui/components/application/LegalInfo.tsx b/src/webui/components/application/LegalInfo.tsx index 428ba91c..4bfb1e1e 100644 --- a/src/webui/components/application/LegalInfo.tsx +++ b/src/webui/components/application/LegalInfo.tsx @@ -8,7 +8,7 @@ export default function Component() By getting in, you agree to our terms of service and privacy policy. - Copyright © 2024 - Masking Technology. + Copyright © 2025 - Masking Technology. ; } diff --git a/src/webui/components/common/TenantContainer.tsx b/src/webui/components/common/TenantContainer.tsx new file mode 100644 index 00000000..4e54256f --- /dev/null +++ b/src/webui/components/common/TenantContainer.tsx @@ -0,0 +1,29 @@ + +import { useEffect, type ReactNode } from 'react'; + +import { useTenant } from './hooks/useTenant'; + +type Props = { + children: ReactNode; +}; + +export default function Component({ children }: Props) +{ + const [tenant] = useTenant(); + + useEffect(() => + { + if (tenant === undefined) return; + + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('href', `/assets/${tenant.id}.css`); + + document.head.appendChild(link); + + }, [tenant]); + + if (tenant === undefined) return null; + + return children; +} diff --git a/src/webui/components/common/hooks/useTenant.ts b/src/webui/components/common/hooks/useTenant.ts new file mode 100644 index 00000000..f21da56b --- /dev/null +++ b/src/webui/components/common/hooks/useTenant.ts @@ -0,0 +1,18 @@ + +import { useCallback } from 'react'; + +import { tenant } from '^/domain/tenant'; +import getByOriginConverted from '^/domain/tenant/getByOriginConverted'; + +import { useLoadData } from '^/webui/hooks'; + +export function useTenant() +{ + const getTenant = useCallback(async () => + { + return await getByOriginConverted(tenant.origin); + + }, []); + + return useLoadData(getTenant, []); +} diff --git a/src/webui/components/index.ts b/src/webui/components/index.ts index 1cf71646..461238e8 100644 --- a/src/webui/components/index.ts +++ b/src/webui/components/index.ts @@ -19,6 +19,7 @@ export { default as OrderRow } from './common/OrderRow'; export { default as PullToRefresh } from './common/PullToRefresh'; export { default as ResultSet } from './common/ResultSet'; export { default as ScrollLoader } from './common/ScrollLoader'; +export { default as TenantContainer } from './common/TenantContainer'; export { default as CreatorFullNameForm } from './creator/FullNameForm'; export { default as CreatorNicknameForm } from './creator/NicknameForm'; export { default as NotificationPanelList } from './notification/PanelList'; diff --git a/src/webui/contexts/AppContext.tsx b/src/webui/contexts/AppContext.tsx index 9b6ff325..a12e2843 100644 --- a/src/webui/contexts/AppContext.tsx +++ b/src/webui/contexts/AppContext.tsx @@ -1,5 +1,5 @@ -import type { ReactNode} from 'react'; +import type { ReactNode } from 'react'; import { createContext, useContext } from 'react'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; diff --git a/src/webui/editor/model/Bubble.ts b/src/webui/editor/model/Bubble.ts index 6ed51248..add7b3cb 100644 --- a/src/webui/editor/model/Bubble.ts +++ b/src/webui/editor/model/Bubble.ts @@ -1,6 +1,6 @@ import Element from '../elements/Element'; -import { type Point } from '../utils/Geometry'; +import type { Point } from '../utils/Geometry'; export default abstract class Bubble extends Element { diff --git a/src/webui/features/hooks/useAddComicPost.ts b/src/webui/features/hooks/useAddComicPost.ts index e188954a..f7e34786 100644 --- a/src/webui/features/hooks/useAddComicPost.ts +++ b/src/webui/features/hooks/useAddComicPost.ts @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { requester } from '^/domain/authentication'; import createPostWithComic from '^/domain/post/createWithComic'; +import { tenant } from '^/domain/tenant'; import { useAppContext } from '^/webui/contexts'; @@ -14,7 +15,7 @@ export default function useAddComicPost() return useCallback(async (imageData: string) => { - await createPostWithComic(requester, imageData); + await createPostWithComic(tenant, requester, imageData); navigate(`/profile/${identity?.nickname}`); diff --git a/src/webui/features/hooks/useCreatePostComicReaction.ts b/src/webui/features/hooks/useCreatePostComicReaction.ts index dd51ff41..f65f6792 100644 --- a/src/webui/features/hooks/useCreatePostComicReaction.ts +++ b/src/webui/features/hooks/useCreatePostComicReaction.ts @@ -5,13 +5,14 @@ import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedPostData } from '^/domain/post/aggregate'; import createComicReaction from '^/domain/post/createWithComic'; import getReaction from '^/domain/post/getByIdAggregated'; +import { tenant } from '^/domain/tenant'; export default function useCreatePostComicReaction(post: AggregatedPostData, handleDone: (reaction?: AggregatedPostData) => void) { return useCallback(async (imageData: string) => { - const reactionId = await createComicReaction(requester, imageData, post.id); - const reaction = await getReaction(requester, reactionId); + const reactionId = await createComicReaction(tenant, requester, imageData, post.id); + const reaction = await getReaction(tenant, requester, reactionId); handleDone(reaction); diff --git a/src/webui/features/hooks/useCreatePostCommentReaction.ts b/src/webui/features/hooks/useCreatePostCommentReaction.ts index c0c0d784..3818eb27 100644 --- a/src/webui/features/hooks/useCreatePostCommentReaction.ts +++ b/src/webui/features/hooks/useCreatePostCommentReaction.ts @@ -5,13 +5,14 @@ import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedPostData } from '^/domain/post/aggregate'; import createCommentReaction from '^/domain/post/createWithComment'; import getReaction from '^/domain/post/getByIdAggregated'; +import { tenant } from '^/domain/tenant'; export default function useCreateCommentReaction(post: AggregatedPostData, handleDone: (reaction?: AggregatedPostData) => void) { return useCallback(async (comment: string) => { - const reactionId = await createCommentReaction(requester, comment, post.id); - const reaction = await getReaction(requester, reactionId); + const reactionId = await createCommentReaction(tenant, requester, comment, post.id); + const reaction = await getReaction(tenant, requester, reactionId); handleDone(reaction); diff --git a/src/webui/features/hooks/useCreator.ts b/src/webui/features/hooks/useCreator.ts index 5d13bb66..27a0b0d7 100644 --- a/src/webui/features/hooks/useCreator.ts +++ b/src/webui/features/hooks/useCreator.ts @@ -2,9 +2,10 @@ import { useCallback } from 'react'; import { useParams } from 'react-router-dom'; -import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; +import { requester } from '^/domain/authentication'; import getCreator from '^/domain/creator/getByNicknameAggregated'; import getRelation from '^/domain/relation/getAggregated'; +import { tenant } from '^/domain/tenant'; import { useAppContext } from '^/webui/contexts'; import { useLoadData } from '^/webui/hooks'; @@ -21,9 +22,9 @@ export default function useCreator() return undefined; } - const creator: AggregatedCreatorData = await getCreator(nickname); + const creator = await getCreator(tenant, requester, nickname); - return getRelation(identity.id, creator.id); + return getRelation(tenant, requester, identity.id, creator.id); }, [identity, nickname]); diff --git a/src/webui/features/hooks/useCreatorFollowers.ts b/src/webui/features/hooks/useCreatorFollowers.ts index 563c2935..c1d7183a 100644 --- a/src/webui/features/hooks/useCreatorFollowers.ts +++ b/src/webui/features/hooks/useCreatorFollowers.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; import getFollowers from '^/domain/relation/getFollowersAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -13,7 +14,7 @@ export default function useCreatorFollowers(creator: AggregatedCreatorData) const getData = useCallback((page: number) => { - return getFollowers(requester, creator.id, { limit, offset: page * limit }); + return getFollowers(tenant, requester, creator.id, { limit, offset: page * limit }); }, [creator]); diff --git a/src/webui/features/hooks/useCreatorFollowing.ts b/src/webui/features/hooks/useCreatorFollowing.ts index 0aaf924f..b9c846cd 100644 --- a/src/webui/features/hooks/useCreatorFollowing.ts +++ b/src/webui/features/hooks/useCreatorFollowing.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; import getFollowing from '^/domain/relation/getFollowingAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -13,7 +14,7 @@ export default function useCreatorFollowing(creator: AggregatedCreatorData) const getData = useCallback((page: number) => { - return getFollowing(requester, creator.id, { limit, offset: page * limit }); + return getFollowing(tenant, requester, creator.id, { limit, offset: page * limit }); }, [creator]); diff --git a/src/webui/features/hooks/useCreatorPosts.ts b/src/webui/features/hooks/useCreatorPosts.ts index 4fe60905..2a20ab95 100644 --- a/src/webui/features/hooks/useCreatorPosts.ts +++ b/src/webui/features/hooks/useCreatorPosts.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; import getCreatorPosts from '^/domain/post/getByCreatorAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -13,7 +14,7 @@ export default function useCreatorPosts(creator: AggregatedCreatorData) const getData = useCallback((page: number) => { - return getCreatorPosts(requester, creator.id, { limit, offset: page * limit }); + return getCreatorPosts(tenant, requester, creator.id, { limit, offset: page * limit }); }, [creator]); diff --git a/src/webui/features/hooks/useEstablishRelation.ts b/src/webui/features/hooks/useEstablishRelation.ts index b72fc8ee..65eab050 100644 --- a/src/webui/features/hooks/useEstablishRelation.ts +++ b/src/webui/features/hooks/useEstablishRelation.ts @@ -4,12 +4,13 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedRelationData } from '^/domain/relation/aggregate'; import establishRelation from '^/domain/relation/establish'; +import { tenant } from '^/domain/tenant'; export default function useEstablishRelation() { return useCallback((relation: AggregatedRelationData) => { - return establishRelation(requester, relation.following.id); + return establishRelation(tenant, requester, relation.following.id); }, []); } diff --git a/src/webui/features/hooks/useExploreCreators.ts b/src/webui/features/hooks/useExploreCreators.ts index c3ba3b70..1321f238 100644 --- a/src/webui/features/hooks/useExploreCreators.ts +++ b/src/webui/features/hooks/useExploreCreators.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import exploreRelations from '^/domain/relation/exploreAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -12,7 +13,7 @@ export default function useExploreCreators() const getData = useCallback((page: number) => { - return exploreRelations(requester, 'popular', { limit, offset: page * limit }); + return exploreRelations(tenant, requester, 'popular', { limit, offset: page * limit }); }, []); diff --git a/src/webui/features/hooks/useExplorePosts.ts b/src/webui/features/hooks/useExplorePosts.ts index 3a25f126..fad0bf1c 100644 --- a/src/webui/features/hooks/useExplorePosts.ts +++ b/src/webui/features/hooks/useExplorePosts.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import explorePosts from '^/domain/post/exploreAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -12,7 +13,7 @@ export default function useExplorePosts() const getData = useCallback((page: number) => { - return explorePosts(requester, { limit, offset: page * limit }); + return explorePosts(tenant, requester, { limit, offset: page * limit }); }, []); diff --git a/src/webui/features/hooks/useHighlight.ts b/src/webui/features/hooks/useHighlight.ts index ee6a9722..6da4335c 100644 --- a/src/webui/features/hooks/useHighlight.ts +++ b/src/webui/features/hooks/useHighlight.ts @@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'; import requester from '^/domain/authentication/requester'; import get from '^/domain/post/getByIdAggregated'; +import { tenant } from '^/domain/tenant'; import { useLoadData } from '^/webui/hooks'; @@ -14,7 +15,7 @@ export default function useReaction() const getReaction = useCallback(async () => { return highlightId !== undefined - ? get(requester, highlightId) + ? get(tenant, requester, highlightId) : undefined; }, [highlightId]); diff --git a/src/webui/features/hooks/useIdentify.ts b/src/webui/features/hooks/useIdentify.ts index 42e2956b..004c0920 100644 --- a/src/webui/features/hooks/useIdentify.ts +++ b/src/webui/features/hooks/useIdentify.ts @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; import getMe from '^/domain/creator/getMeAggregated'; +import { tenant } from '^/domain/tenant'; import { useAppContext } from '^/webui/contexts'; @@ -26,7 +27,7 @@ export default function useIdentify() const getIdentity = async () => { - const identity = await getMe(requester); + const identity = await getMe(tenant, requester); setIdentity(identity); }; diff --git a/src/webui/features/hooks/useNotifications.ts b/src/webui/features/hooks/useNotifications.ts index abc31b9b..330bbd73 100644 --- a/src/webui/features/hooks/useNotifications.ts +++ b/src/webui/features/hooks/useNotifications.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import getRecentNotifications from '^/domain/notification/getRecentAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -12,7 +13,7 @@ export default function useNotifications() const getNotifications = useCallback((page: number) => { - return getRecentNotifications(requester, { limit, offset: page * limit }); + return getRecentNotifications(tenant, requester, { limit, offset: page * limit }); }, []); diff --git a/src/webui/features/hooks/usePost.ts b/src/webui/features/hooks/usePost.ts index a9f2cd7b..3ec6fe71 100644 --- a/src/webui/features/hooks/usePost.ts +++ b/src/webui/features/hooks/usePost.ts @@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'; import { requester } from '^/domain/authentication'; import get from '^/domain/post/getByIdAggregated'; +import { tenant } from '^/domain/tenant'; import { useLoadData } from '^/webui/hooks'; @@ -14,7 +15,7 @@ export default function usePost() const getPost = useCallback(async () => { return postId !== undefined - ? get(requester, postId) + ? get(tenant, requester, postId) : undefined; }, [postId]); diff --git a/src/webui/features/hooks/usePostReactions.ts b/src/webui/features/hooks/usePostReactions.ts index 4aa5fc18..085aafd8 100644 --- a/src/webui/features/hooks/usePostReactions.ts +++ b/src/webui/features/hooks/usePostReactions.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedPostData } from '^/domain/post/aggregate'; import getReactionsByPost from '^/domain/post/getByParentAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -13,7 +14,7 @@ export default function usePostReactions(post: AggregatedPostData) const getData = useCallback((page: number) => { - return getReactionsByPost(requester, post.id, { limit, offset: page * limit }); + return getReactionsByPost(tenant, requester, post.id, { limit, offset: page * limit }); }, [post]); diff --git a/src/webui/features/hooks/usePostsFollowing.ts b/src/webui/features/hooks/usePostsFollowing.ts index 0d361b25..212988ce 100644 --- a/src/webui/features/hooks/usePostsFollowing.ts +++ b/src/webui/features/hooks/usePostsFollowing.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import getPostsFollowing from '^/domain/post/getByFollowingAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -12,7 +13,7 @@ export default function usePostsFollowing() const getData = useCallback((page: number) => { - return getPostsFollowing(requester, { limit, offset: page * limit }); + return getPostsFollowing(tenant, requester, { limit, offset: page * limit }); }, []); diff --git a/src/webui/features/hooks/usePostsRecommended.ts b/src/webui/features/hooks/usePostsRecommended.ts index a5119bdc..e01d8c6a 100644 --- a/src/webui/features/hooks/usePostsRecommended.ts +++ b/src/webui/features/hooks/usePostsRecommended.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import getPostsRecommended from '^/domain/post/getRecommendedAggregated'; +import { tenant } from '^/domain/tenant'; import { usePagination } from '^/webui/hooks'; @@ -12,7 +13,7 @@ export default function usePostsRecommended() const getData = useCallback((page: number) => { - return getPostsRecommended(requester, { limit, offset: page * limit }); + return getPostsRecommended(tenant, requester, { limit, offset: page * limit }); }, []); diff --git a/src/webui/features/hooks/useRemovePost.ts b/src/webui/features/hooks/useRemovePost.ts index 13398a8e..fd441f2d 100644 --- a/src/webui/features/hooks/useRemovePost.ts +++ b/src/webui/features/hooks/useRemovePost.ts @@ -7,6 +7,7 @@ import { useAppContext } from '^/webui/contexts'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedPostData } from '^/domain/post/aggregate'; import remove from '^/domain/post/remove'; +import { tenant } from '^/domain/tenant'; export default function useRemovePost() { @@ -15,7 +16,7 @@ export default function useRemovePost() return useCallback(async (post: AggregatedPostData) => { - await remove(requester, post.id); + await remove(tenant, requester, post.id); navigate(`/profile/${identity?.nickname}`); diff --git a/src/webui/features/hooks/useTogglePostRating.ts b/src/webui/features/hooks/useTogglePostRating.ts index 759a21d8..13ba8ed1 100644 --- a/src/webui/features/hooks/useTogglePostRating.ts +++ b/src/webui/features/hooks/useTogglePostRating.ts @@ -4,12 +4,13 @@ import { useCallback } from 'react'; import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedPostData } from '^/domain/post/aggregate'; import toggleRating from '^/domain/rating/toggle'; +import { tenant } from '^/domain/tenant'; export default function useTogglePostRating() { return useCallback((post: AggregatedPostData) => { - return toggleRating(requester, post.id); + return toggleRating(tenant, requester, post.id); }, []); } diff --git a/src/webui/features/hooks/useUpdateNickname.ts b/src/webui/features/hooks/useUpdateNickname.ts index 78a8fb2e..9a74d3a7 100644 --- a/src/webui/features/hooks/useUpdateNickname.ts +++ b/src/webui/features/hooks/useUpdateNickname.ts @@ -5,6 +5,7 @@ import { requester } from '^/domain/authentication'; import type { AggregatedData as AggregatedCreatorData } from '^/domain/creator/aggregate'; import updateNickname from '^/domain/creator/updateNickname'; import NicknameAlreadyExists from '^/domain/creator/updateNickname/NicknameAlreadyExists'; +import { tenant } from '^/domain/tenant'; import { useAppContext } from '^/webui/contexts'; @@ -17,7 +18,7 @@ export default function useUpdateNickname() { try { - await updateNickname(requester, nickname); + await updateNickname(tenant, requester, nickname); setIdentity({ ...identity, nickname } as AggregatedCreatorData); setAlreadyInUse(false); diff --git a/src/webui/main.tsx b/src/webui/main.tsx index 70d85189..cd143246 100644 --- a/src/webui/main.tsx +++ b/src/webui/main.tsx @@ -3,6 +3,7 @@ import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import { TenantContainer } from './components'; import { AppContextProvider } from './contexts'; import './designsystem/designsystem.css'; @@ -10,8 +11,10 @@ import './main.css'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - + + + + + ); diff --git a/test/domain/authentication/fixtures/index.ts b/test/domain/authentication/fixtures/index.ts index 875f8973..cef99487 100644 --- a/test/domain/authentication/fixtures/index.ts +++ b/test/domain/authentication/fixtures/index.ts @@ -5,5 +5,5 @@ export * from './httpClients.fixture'; export * from './identities.fixture'; export * from './images.fixture'; export * from './records.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; - diff --git a/test/domain/authentication/fixtures/records.fixture.ts b/test/domain/authentication/fixtures/records.fixture.ts index 37428def..297719d5 100644 --- a/test/domain/authentication/fixtures/records.fixture.ts +++ b/test/domain/authentication/fixtures/records.fixture.ts @@ -3,9 +3,10 @@ import type { RecordData } from '^/integrations/database'; import type { DataModel as CreatorDataModel } from '^/domain/creator'; +import { TENANTS } from './tenants.fixture'; import { VALUES } from './values.fixture'; -const DEFAULT_DATA = { portraitId: undefined, joinedAt: new Date().toISOString() }; +const DEFAULT_DATA = { tenantId: TENANTS.default.id, portraitId: undefined, joinedAt: new Date().toISOString() }; const CREATORS: CreatorDataModel[] = [ { id: VALUES.IDS.FIRST, fullName: VALUES.FULL_NAMES.FIRST, nickname: VALUES.NICKNAMES.FIRST, email: VALUES.EMAILS.FIRST, ...DEFAULT_DATA }, diff --git a/test/domain/authentication/fixtures/tenants.fixture.ts b/test/domain/authentication/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/authentication/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/authentication/login.spec.ts b/test/domain/authentication/login.spec.ts index 394bf6f6..679680ad 100644 --- a/test/domain/authentication/login.spec.ts +++ b/test/domain/authentication/login.spec.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import login from '^/domain/authentication/login'; import { TooManySimilarNicknames } from '^/domain/creator/generateNickname'; -import { DATABASES, FILE_STORES, HTTP_CLIENTS, IDENTITIES, VALUES } from './fixtures'; +import { DATABASES, FILE_STORES, HTTP_CLIENTS, IDENTITIES, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -18,54 +18,54 @@ beforeEach(async () => describe('domain/authentication', () => { - describe('.login(identity)', () => + describe('.login(tenant, identity)', () => { it('should login with an existing email', async () => { - const requester = await login(IDENTITIES.EXISTING); + const requester = await login(TENANTS.default, IDENTITIES.EXISTING); expect(requester.nickname).toBe(VALUES.NICKNAMES.FIRST); }); it('should register without a nickname', async () => { - const requester = await login(IDENTITIES.NO_NICKNAME); + const requester = await login(TENANTS.default, IDENTITIES.NO_NICKNAME); expect(requester.nickname).toBe(VALUES.NICKNAMES.FROM_FULL_NAME); }); it('should register with a duplicate nickname', async () => { - const requester = await login(IDENTITIES.DUPLICATE_NICKNAME); + const requester = await login(TENANTS.default, IDENTITIES.DUPLICATE_NICKNAME); expect(requester.nickname).toBe(VALUES.NICKNAMES.DEDUPLICATED); }); it('should register with multiple occurrences of nickname', async () => { - const requester = await login(IDENTITIES.MULTIPLE_OCCURRENCES_NICKNAME); + const requester = await login(TENANTS.default, IDENTITIES.MULTIPLE_OCCURRENCES_NICKNAME); expect(requester.nickname).toBe(VALUES.NICKNAMES.NEXT_OCCURRED); }); it('should NOT register with too many occurrences nickname', async () => { - const promise = login(IDENTITIES.TOO_MANY_SIMILAR_NICKNAMES); + const promise = login(TENANTS.default, IDENTITIES.TOO_MANY_SIMILAR_NICKNAMES); await expect(promise).rejects.toStrictEqual(new TooManySimilarNicknames()); }); it('should register with spaces in nickname', async () => { - const requester = await login(IDENTITIES.SPACED_NICKNAME); + const requester = await login(TENANTS.default, IDENTITIES.SPACED_NICKNAME); expect(requester.nickname).toBe(VALUES.NICKNAMES.DESPACED); }); it('should register with underscores in nickname', async () => { - const requester = await login(IDENTITIES.UNDERSCORED_NICKNAME); + const requester = await login(TENANTS.default, IDENTITIES.UNDERSCORED_NICKNAME); expect(requester.nickname).toBe(VALUES.NICKNAMES.DEUNDERSCORED); }); it('should register with a valid profile picture', async () => { - const requestor = await login(IDENTITIES.WITH_PICTURE); - expect(requestor.nickname).toBe(VALUES.NICKNAMES.WITH_PICTURE); + const requester = await login(TENANTS.default, IDENTITIES.WITH_PICTURE); + expect(requester.nickname).toBe(VALUES.NICKNAMES.WITH_PICTURE); }); }); }); diff --git a/test/domain/creator/fixtures/index.ts b/test/domain/creator/fixtures/index.ts index ad06c48a..0b405ed6 100644 --- a/test/domain/creator/fixtures/index.ts +++ b/test/domain/creator/fixtures/index.ts @@ -2,4 +2,5 @@ export * from './databases.fixture'; export * from './records.fixture'; export * from './requesters.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; diff --git a/test/domain/creator/fixtures/records.fixture.ts b/test/domain/creator/fixtures/records.fixture.ts index 18b3fc36..3fd20407 100644 --- a/test/domain/creator/fixtures/records.fixture.ts +++ b/test/domain/creator/fixtures/records.fixture.ts @@ -3,9 +3,10 @@ import type { RecordData } from '^/integrations/database'; import type { DataModel as CreatorDataModel } from '^/domain/creator'; +import { TENANTS } from './tenants.fixture'; import { VALUES } from './values.fixture'; -const DEFAULT_DATA = { portraitId: undefined, joinedAt: new Date().toISOString() }; +const DEFAULT_DATA = { tenantId: TENANTS.default.id, portraitId: undefined, joinedAt: new Date().toISOString() }; const CREATORS: CreatorDataModel[] = [ { id: VALUES.IDS.CREATOR, fullName: VALUES.FULL_NAMES.CREATOR, nickname: VALUES.NICKNAMES.CREATOR, email: VALUES.EMAILS.CREATOR, ...DEFAULT_DATA } diff --git a/test/domain/creator/fixtures/tenants.fixture.ts b/test/domain/creator/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/creator/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/creator/updateNickname.spec.ts b/test/domain/creator/updateNickname.spec.ts index 6bc36368..eeb07147 100644 --- a/test/domain/creator/updateNickname.spec.ts +++ b/test/domain/creator/updateNickname.spec.ts @@ -6,7 +6,7 @@ import updateNickname, { NicknameAlreadyExists } from '^/domain/creator/updateNi import database from '^/integrations/database'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -17,7 +17,7 @@ describe('domain/creator/updateNickname', () => { it('should update the nickname', async () => { - await updateNickname(REQUESTERS.CREATOR, VALUES.NICKNAMES.NEW); + await updateNickname(TENANTS.default, REQUESTERS.CREATOR, VALUES.NICKNAMES.NEW); const creator = await database.readRecord(CREATOR_RECORD_TYPE, REQUESTERS.CREATOR.id); expect(creator?.nickname).toBe(VALUES.NICKNAMES.NEW); @@ -25,7 +25,7 @@ describe('domain/creator/updateNickname', () => it('should NOT update the nickname because of a duplicate', async () => { - const promise = updateNickname(REQUESTERS.CREATOR, VALUES.NICKNAMES.DUPLICATE); + const promise = updateNickname(TENANTS.default, REQUESTERS.CREATOR, VALUES.NICKNAMES.DUPLICATE); await expect(promise).rejects.toStrictEqual(new NicknameAlreadyExists(VALUES.NICKNAMES.DUPLICATE)); }); diff --git a/test/domain/notification/fixtures/index.ts b/test/domain/notification/fixtures/index.ts index 9d946137..e7fd662c 100644 --- a/test/domain/notification/fixtures/index.ts +++ b/test/domain/notification/fixtures/index.ts @@ -4,5 +4,5 @@ export * from './dataUrls.fixture'; export * from './fileStores.fixture'; export * from './records.fixture'; export * from './requesters.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; - diff --git a/test/domain/notification/fixtures/records.fixture.ts b/test/domain/notification/fixtures/records.fixture.ts index 04ed7602..c94d6806 100644 --- a/test/domain/notification/fixtures/records.fixture.ts +++ b/test/domain/notification/fixtures/records.fixture.ts @@ -12,12 +12,13 @@ import type { DataModel as PostMetricsModel } from '^/domain/post.metrics'; import type { DataModel as RatingDataModel } from '^/domain/rating'; import type { DataModel as RelationDataModel } from '^/domain/relation'; +import { TENANTS } from './tenants.fixture'; import { VALUES } from './values.fixture'; const CREATORS: CreatorDataModel[] = [ - { id: VALUES.IDS.CREATOR1, fullName: VALUES.FULL_NAMES.CREATOR1, nickname: VALUES.NICKNAMES.CREATOR1, email: VALUES.EMAILS.CREATOR1, portraitId: undefined, joinedAt: new Date().toISOString() }, - { id: VALUES.IDS.CREATOR2, fullName: VALUES.FULL_NAMES.CREATOR2, nickname: VALUES.NICKNAMES.CREATOR2, email: VALUES.EMAILS.CREATOR2, portraitId: undefined, joinedAt: new Date().toISOString() }, - { id: VALUES.IDS.CREATOR3, fullName: VALUES.FULL_NAMES.CREATOR3, nickname: VALUES.NICKNAMES.CREATOR3, email: VALUES.EMAILS.CREATOR3, portraitId: undefined, joinedAt: new Date().toISOString() } + { id: VALUES.IDS.CREATOR1, fullName: VALUES.FULL_NAMES.CREATOR1, nickname: VALUES.NICKNAMES.CREATOR1, email: VALUES.EMAILS.CREATOR1, portraitId: undefined, tenantId: TENANTS.default.id, joinedAt: new Date().toISOString() }, + { id: VALUES.IDS.CREATOR2, fullName: VALUES.FULL_NAMES.CREATOR2, nickname: VALUES.NICKNAMES.CREATOR2, email: VALUES.EMAILS.CREATOR2, portraitId: undefined, tenantId: TENANTS.default.id, joinedAt: new Date().toISOString() }, + { id: VALUES.IDS.CREATOR3, fullName: VALUES.FULL_NAMES.CREATOR3, nickname: VALUES.NICKNAMES.CREATOR3, email: VALUES.EMAILS.CREATOR3, portraitId: undefined, tenantId: TENANTS.default.id, joinedAt: new Date().toISOString() } ]; const CREATOR_METRICS: CreatorMetricsDataModel[] = [ @@ -40,9 +41,9 @@ const COMICS: ComicDataModel[] = [ ]; const POSTS: (PostDataModel & { deleted: boolean; })[] = [ - { id: VALUES.IDS.POST_RATED, creatorId: VALUES.IDS.CREATOR1, comicId: VALUES.IDS.COMIC, createdAt: new Date().toISOString(), deleted: false }, - { id: VALUES.IDS.POST_DELETED, creatorId: VALUES.IDS.CREATOR1, comicId: VALUES.IDS.COMIC, createdAt: new Date().toISOString(), deleted: true }, - { id: VALUES.IDS.REACTION_LIKED, creatorId: VALUES.IDS.CREATOR2, comicId: VALUES.IDS.COMIC, parentId: VALUES.IDS.POST_RATED, createdAt: new Date().toISOString(), deleted: false } + { id: VALUES.IDS.POST_RATED, creatorId: VALUES.IDS.CREATOR1, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: new Date().toISOString(), deleted: false }, + { id: VALUES.IDS.POST_DELETED, creatorId: VALUES.IDS.CREATOR1, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: new Date().toISOString(), deleted: true }, + { id: VALUES.IDS.REACTION_LIKED, creatorId: VALUES.IDS.CREATOR2, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, parentId: VALUES.IDS.POST_RATED, createdAt: new Date().toISOString(), deleted: false } ]; const POST_METRICS: PostMetricsModel[] = [ diff --git a/test/domain/notification/fixtures/tenants.fixture.ts b/test/domain/notification/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/notification/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/notification/getRecentAggregated.spec.ts b/test/domain/notification/getRecentAggregated.spec.ts index 13e40f61..cb9a8687 100644 --- a/test/domain/notification/getRecentAggregated.spec.ts +++ b/test/domain/notification/getRecentAggregated.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { Types } from '^/domain/notification'; import getRecentAggregated from '^/domain/notification/getRecentAggregated'; -import { DATABASES, FILE_STORES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, FILE_STORES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -17,7 +17,7 @@ describe('domain/notification/getallAggregated', () => { it('should give all notifications under normal circumstances', async () => { - const result = await getRecentAggregated(REQUESTERS.CREATOR2, { offset: 0, limit: 7 }); + const result = await getRecentAggregated(TENANTS.default, REQUESTERS.CREATOR2, { offset: 0, limit: 7 }); expect(result).toHaveLength(2); @@ -36,7 +36,7 @@ describe('domain/notification/getallAggregated', () => it('should give only the notifications that aggregate without errors', async () => { - const result = await getRecentAggregated(REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); + const result = await getRecentAggregated(TENANTS.default, REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); expect(result).toHaveLength(2); diff --git a/test/domain/post/createWithComic.spec.ts b/test/domain/post/createWithComic.spec.ts index 1c577991..3bc7c820 100644 --- a/test/domain/post/createWithComic.spec.ts +++ b/test/domain/post/createWithComic.spec.ts @@ -6,7 +6,7 @@ import createWithComic from '^/domain/post/createWithComic'; import database from '^/integrations/database'; -import { DATABASES, DATA_URLS, FILE_STORES, REQUESTERS } from './fixtures'; +import { DATABASES, DATA_URLS, FILE_STORES, REQUESTERS, TENANTS } from './fixtures'; beforeEach(async () => { @@ -20,7 +20,7 @@ describe('domain/post/add', () => { it('should create a post', async () => { - await createWithComic(REQUESTERS.CREATOR1, DATA_URLS.COMIC_IMAGE); + await createWithComic(TENANTS.default, REQUESTERS.CREATOR1, DATA_URLS.COMIC_IMAGE); const posts = await database.searchRecords(POST_RECORD_TYPE, {}); expect(posts.length).toBe(1); diff --git a/test/domain/post/fixtures/databases.fixture.ts b/test/domain/post/fixtures/databases.fixture.ts index c4c15c7d..31fd61e8 100644 --- a/test/domain/post/fixtures/databases.fixture.ts +++ b/test/domain/post/fixtures/databases.fixture.ts @@ -11,11 +11,11 @@ import database from '^/integrations/database'; import { RECORDS } from './records.fixture'; -database.connect(); +await database.connect(); async function withCreators(): Promise { - database.clear(); + await database.clear(); const promises = RECORDS.CREATORS.map(creator => database.createRecord(CREATOR_RECORD_TYPE, { ...creator })); @@ -24,7 +24,7 @@ async function withCreators(): Promise async function withCreatorsPostsAndRelations(): Promise { - database.clear(); + await database.clear(); const promises = [ RECORDS.CREATORS.map(creator => database.createRecord(CREATOR_RECORD_TYPE, { ...creator })), @@ -41,7 +41,7 @@ async function withCreatorsPostsAndRelations(): Promise async function withPostsAndCreators(): Promise { - database.clear(); + await database.clear(); const promises = [ RECORDS.CREATORS.map(creator => database.createRecord(CREATOR_RECORD_TYPE, { ...creator })), diff --git a/test/domain/post/fixtures/index.ts b/test/domain/post/fixtures/index.ts index 44991aaa..098e997b 100644 --- a/test/domain/post/fixtures/index.ts +++ b/test/domain/post/fixtures/index.ts @@ -5,4 +5,5 @@ export * from './fileStores.fixture'; export * from './queries.fixture'; export * from './records.fixture'; export * from './requesters.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; diff --git a/test/domain/post/fixtures/records.fixture.ts b/test/domain/post/fixtures/records.fixture.ts index d74617d3..06fa3728 100644 --- a/test/domain/post/fixtures/records.fixture.ts +++ b/test/domain/post/fixtures/records.fixture.ts @@ -11,13 +11,14 @@ import type { DataModel as RatingDataModel } from '^/domain/rating'; import type { DataModel as RelationDataModel } from '^/domain/relation'; import { REQUESTERS } from './requesters.fixture'; +import { TENANTS } from './tenants.fixture'; import { VALUES } from './values.fixture'; const NOW = new Date().toISOString(); const CREATORS: CreatorDataModel[] = [ - { id: VALUES.IDS.CREATOR1, fullName: VALUES.FULL_NAMES.CREATOR1, nickname: VALUES.NICKNAMES.CREATOR1, email: VALUES.EMAILS.CREATOR1, joinedAt: NOW }, - { id: VALUES.IDS.CREATOR2, fullName: VALUES.FULL_NAMES.CREATOR2, nickname: VALUES.NICKNAMES.CREATOR2, email: VALUES.EMAILS.CREATOR2, joinedAt: NOW } + { id: VALUES.IDS.CREATOR1, fullName: VALUES.FULL_NAMES.CREATOR1, nickname: VALUES.NICKNAMES.CREATOR1, email: VALUES.EMAILS.CREATOR1, tenantId: TENANTS.default.id, joinedAt: NOW }, + { id: VALUES.IDS.CREATOR2, fullName: VALUES.FULL_NAMES.CREATOR2, nickname: VALUES.NICKNAMES.CREATOR2, email: VALUES.EMAILS.CREATOR2, tenantId: TENANTS.default.id, joinedAt: NOW } ]; const CREATOR_METRICS: CreatorMetricsDataModel[] = [ @@ -38,10 +39,10 @@ const COMICS: ComicDataModel[] = [ ]; const POSTS: (PostDataModel & { deleted: boolean; })[] = [ - { id: VALUES.IDS.POST_RATED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, createdAt: NOW, deleted: false }, - { id: VALUES.IDS.POST_UNRATED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, createdAt: NOW, deleted: false }, - { id: VALUES.IDS.POST_EXTRA1, creatorId: REQUESTERS.CREATOR2.id, comicId: VALUES.IDS.COMIC, createdAt: NOW, deleted: false }, - { id: VALUES.IDS.POST_DELETED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, createdAt: NOW, deleted: true }, + { id: VALUES.IDS.POST_RATED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: NOW, deleted: false }, + { id: VALUES.IDS.POST_UNRATED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: NOW, deleted: false }, + { id: VALUES.IDS.POST_EXTRA1, creatorId: REQUESTERS.CREATOR2.id, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: NOW, deleted: false }, + { id: VALUES.IDS.POST_DELETED, creatorId: REQUESTERS.CREATOR1.id, comicId: VALUES.IDS.COMIC, tenantId: TENANTS.default.id, createdAt: NOW, deleted: true }, ]; const POST_METRICS: PostMetricsDataModel[] = [ diff --git a/test/domain/post/fixtures/tenants.fixture.ts b/test/domain/post/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/post/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/post/getByFollowingAggregated.spec.ts b/test/domain/post/getByFollowingAggregated.spec.ts index 436a53dd..b4e6c46a 100644 --- a/test/domain/post/getByFollowingAggregated.spec.ts +++ b/test/domain/post/getByFollowingAggregated.spec.ts @@ -1,7 +1,7 @@ import getByFollowingAggregated from '^/domain/post/getByFollowingAggregated'; import { beforeEach, describe, expect, it } from 'vitest'; -import { DATABASES, FILE_STORES, REQUESTERS } from './fixtures'; +import { DATABASES, FILE_STORES, REQUESTERS, TENANTS } from './fixtures'; beforeEach(async () => { @@ -15,7 +15,7 @@ describe('domain/post/getByFollowingAggregated', () => { it('should get posts from everyone followed by the requester', async () => { - const result = await getByFollowingAggregated(REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); + const result = await getByFollowingAggregated(TENANTS.default, REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); expect(result).toHaveLength(1); expect(result[0].creator.following.id).toBe(REQUESTERS.CREATOR2.id); diff --git a/test/domain/post/getRecommendedAggregated.spec.ts b/test/domain/post/getRecommendedAggregated.spec.ts index 5eedc6f8..18fb02d4 100644 --- a/test/domain/post/getRecommendedAggregated.spec.ts +++ b/test/domain/post/getRecommendedAggregated.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import getRecommendedAggregated from '^/domain/post/getRecommendedAggregated'; -import { DATA_URLS, DATABASES, FILE_STORES, REQUESTERS } from './fixtures'; +import { DATA_URLS, DATABASES, FILE_STORES, REQUESTERS, TENANTS } from './fixtures'; beforeEach(async () => { @@ -16,7 +16,7 @@ describe('domain/post/getRecommendedAggregated', () => { it('should give all posts except those created by the requester', async () => { - const result = await getRecommendedAggregated(REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); + const result = await getRecommendedAggregated(TENANTS.default, REQUESTERS.CREATOR1, { offset: 0, limit: 7 }); expect(result).toHaveLength(1); expect(result[0].creator.following.id).toBe(REQUESTERS.CREATOR2.id); diff --git a/test/domain/post/remove.spec.ts b/test/domain/post/remove.spec.ts index b7f97167..8907ba7e 100644 --- a/test/domain/post/remove.spec.ts +++ b/test/domain/post/remove.spec.ts @@ -6,7 +6,7 @@ import remove from '^/domain/post/remove'; import database from '^/integrations/database'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -17,7 +17,7 @@ describe('domain/post/remove', () => { it('should soft delete a post', async () => { - await remove(REQUESTERS.CREATOR1, VALUES.IDS.POST_RATED); + await remove(TENANTS.default, REQUESTERS.CREATOR1, VALUES.IDS.POST_RATED); const reaction = await database.readRecord(RECORD_TYPE, VALUES.IDS.POST_RATED); expect(reaction.deleted).toBeTruthy(); @@ -25,13 +25,13 @@ describe('domain/post/remove', () => it('should not delete an already deleted post', async () => { - const promise = remove(REQUESTERS.CREATOR1, VALUES.IDS.POST_DELETED); + const promise = remove(TENANTS.default, REQUESTERS.CREATOR1, VALUES.IDS.POST_DELETED); await expect(promise).rejects.toThrow(PostNotFound); }); it('should not delete a post from another creator', async () => { - const promise = remove(REQUESTERS.VIEWER, VALUES.IDS.POST_RATED); + const promise = remove(TENANTS.default, REQUESTERS.VIEWER, VALUES.IDS.POST_RATED); await expect(promise).rejects.toThrow(PostNotFound); }); }); diff --git a/test/domain/rating/fixtures/index.ts b/test/domain/rating/fixtures/index.ts index ad06c48a..0b405ed6 100644 --- a/test/domain/rating/fixtures/index.ts +++ b/test/domain/rating/fixtures/index.ts @@ -2,4 +2,5 @@ export * from './databases.fixture'; export * from './records.fixture'; export * from './requesters.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; diff --git a/test/domain/rating/fixtures/tenants.fixture.ts b/test/domain/rating/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/rating/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/rating/toggle.spec.ts b/test/domain/rating/toggle.spec.ts index 4f93773b..4a0be2c2 100644 --- a/test/domain/rating/toggle.spec.ts +++ b/test/domain/rating/toggle.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import toggle from '^/domain/rating/toggle'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -14,13 +14,13 @@ describe('domain/post/toggleRating', () => { it('should add a rating', async () => { - const isRated = await toggle(REQUESTERS.CREATOR1, VALUES.IDS.POST_UNRATED); + const isRated = await toggle(TENANTS.default, REQUESTERS.CREATOR1, VALUES.IDS.POST_UNRATED); expect(isRated).toBeTruthy(); }); it('should remove a rating', async () => { - const isRated = await toggle(REQUESTERS.CREATOR1, VALUES.IDS.POST_RATED); + const isRated = await toggle(TENANTS.default, REQUESTERS.CREATOR1, VALUES.IDS.POST_RATED); expect(isRated).toBeFalsy(); }); }); diff --git a/test/domain/relation/establish.spec.ts b/test/domain/relation/establish.spec.ts index 1e37cc9d..fd5a2553 100644 --- a/test/domain/relation/establish.spec.ts +++ b/test/domain/relation/establish.spec.ts @@ -2,12 +2,11 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { RECORD_TYPE as RELATION_RECORD_TYPE } from '^/domain/relation'; -import { InvalidRelation } from '^/domain/relation/create'; import establish, { RelationAlreadyExists } from '^/domain/relation/establish'; import database from '^/integrations/database'; -import { DATABASES, QUERIES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, QUERIES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -18,7 +17,7 @@ describe('domain/relation/establish', () => { it('should establish a relation', async () => { - await establish(REQUESTERS.SECOND, VALUES.IDS.CREATOR1); + await establish(TENANTS.default, REQUESTERS.SECOND, VALUES.IDS.CREATOR1); const relation = await database.findRecord(RELATION_RECORD_TYPE, QUERIES.EXISTING_RELATION); expect(relation?.id).toBeDefined(); @@ -26,15 +25,8 @@ describe('domain/relation/establish', () => it('should NOT establish a duplicate relation', async () => { - const promise = establish(REQUESTERS.FIRST, VALUES.IDS.CREATOR2); + const promise = establish(TENANTS.default, REQUESTERS.FIRST, VALUES.IDS.CREATOR2); await expect(promise).rejects.toStrictEqual(new RelationAlreadyExists()); }); - - it('should fail when invalid data is provided', async () => - { - const promise = establish(REQUESTERS.FIRST, VALUES.IDS.INVALID); - - await expect(promise).rejects.toThrow(InvalidRelation); - }); }); diff --git a/test/domain/relation/exploreAggregated.spec.ts b/test/domain/relation/exploreAggregated.spec.ts index c70f1904..e525a2f2 100644 --- a/test/domain/relation/exploreAggregated.spec.ts +++ b/test/domain/relation/exploreAggregated.spec.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { SortOrders } from '^/domain/relation/definitions'; import explore from '^/domain/relation/exploreAggregated'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -15,7 +15,7 @@ describe('domain/relation/exploreAggregated', () => { it('should explore relations based on recent', async () => { - const relations = await explore(REQUESTERS.FIRST, SortOrders.RECENT, VALUES.RANGE); + const relations = await explore(TENANTS.default, REQUESTERS.FIRST, SortOrders.RECENT, VALUES.RANGE); expect(relations).toHaveLength(3); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR4); expect(relations[1].following?.id).toBe(VALUES.IDS.CREATOR6); @@ -24,27 +24,27 @@ describe('domain/relation/exploreAggregated', () => it('should find no relations based on search', async () => { - const relations = await explore(REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'or2'); + const relations = await explore(TENANTS.default, REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'or2'); expect(relations).toHaveLength(0); }); it('should find relations based on search full name', async () => { - const relations = await explore(REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'or 4'); + const relations = await explore(TENANTS.default, REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'or 4'); expect(relations).toHaveLength(1); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR4); }); it('should find relations based on search nickname', async () => { - const relations = await explore(REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'creator4'); + const relations = await explore(TENANTS.default, REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'creator4'); expect(relations).toHaveLength(1); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR4); }); it('should find relations based on search full name and nickname', async () => { - const relations = await explore(REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'five'); + const relations = await explore(TENANTS.default, REQUESTERS.FIRST, SortOrders.POPULAR, VALUES.RANGE, 'five'); expect(relations).toHaveLength(2); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR5); expect(relations[1].following?.id).toBe(VALUES.IDS.CREATOR6); diff --git a/test/domain/relation/fixtures/index.ts b/test/domain/relation/fixtures/index.ts index 93c1e1ba..e8454b7b 100644 --- a/test/domain/relation/fixtures/index.ts +++ b/test/domain/relation/fixtures/index.ts @@ -3,4 +3,5 @@ export * from './databases.fixture'; export * from './queries.fixture'; export * from './records.fixture'; export * from './requesters.fixture'; +export * from './tenants.fixture'; export * from './values.fixture'; diff --git a/test/domain/relation/fixtures/records.fixture.ts b/test/domain/relation/fixtures/records.fixture.ts index 09165545..5227826f 100644 --- a/test/domain/relation/fixtures/records.fixture.ts +++ b/test/domain/relation/fixtures/records.fixture.ts @@ -5,15 +5,16 @@ import type { DataModel as CreatorDataModel } from '^/domain/creator'; import type { DataModel as CreatorMetricsDataModel } from '^/domain/creator.metrics'; import type { DataModel as RelationDataModel } from '^/domain/relation'; +import { TENANTS } from './tenants.fixture'; import { VALUES } from './values.fixture'; const CREATORS: CreatorDataModel[] = [ - { id: VALUES.IDS.CREATOR1, fullName: 'Creator 1', nickname: 'creator1', email: 'creator1@mail.com', joinedAt: new Date(2024, 5, 23).toISOString(), portraitId: undefined }, - { id: VALUES.IDS.CREATOR2, fullName: 'Creator 2', nickname: 'creator2', email: 'creator2@mail.com', joinedAt: new Date(2024, 7, 11).toISOString(), portraitId: undefined }, - { id: VALUES.IDS.CREATOR3, fullName: 'Creator 3', nickname: 'creator3', email: 'creator3@mail.com', joinedAt: new Date(2024, 1, 24).toISOString(), portraitId: undefined }, - { id: VALUES.IDS.CREATOR4, fullName: 'Creator 4', nickname: 'creator4', email: 'creator4@mail.com', joinedAt: new Date(2024, 2, 12).toISOString(), portraitId: undefined }, - { id: VALUES.IDS.CREATOR5, fullName: 'Creator five', nickname: 'creator5', email: 'creator5@mail.com', joinedAt: new Date(2024, 4, 9).toISOString(), portraitId: undefined }, - { id: VALUES.IDS.CREATOR6, fullName: 'Creator 6', nickname: 'not_five', email: 'creator6@mail.com', joinedAt: new Date(2024, 3, 18).toISOString(), portraitId: undefined } + { id: VALUES.IDS.CREATOR1, fullName: 'Creator 1', nickname: 'creator1', email: 'creator1@mail.com', joinedAt: new Date(2024, 5, 23).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined }, + { id: VALUES.IDS.CREATOR2, fullName: 'Creator 2', nickname: 'creator2', email: 'creator2@mail.com', joinedAt: new Date(2024, 7, 11).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined }, + { id: VALUES.IDS.CREATOR3, fullName: 'Creator 3', nickname: 'creator3', email: 'creator3@mail.com', joinedAt: new Date(2024, 1, 24).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined }, + { id: VALUES.IDS.CREATOR4, fullName: 'Creator 4', nickname: 'creator4', email: 'creator4@mail.com', joinedAt: new Date(2024, 2, 12).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined }, + { id: VALUES.IDS.CREATOR5, fullName: 'Creator five', nickname: 'creator5', email: 'creator5@mail.com', joinedAt: new Date(2024, 4, 9).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined }, + { id: VALUES.IDS.CREATOR6, fullName: 'Creator 6', nickname: 'not_five', email: 'creator6@mail.com', joinedAt: new Date(2024, 3, 18).toISOString(), tenantId: TENANTS.default.id, portraitId: undefined } ]; const CREATOR_METRICS: CreatorMetricsDataModel[] = [ diff --git a/test/domain/relation/fixtures/tenants.fixture.ts b/test/domain/relation/fixtures/tenants.fixture.ts new file mode 100644 index 00000000..ea725060 --- /dev/null +++ b/test/domain/relation/fixtures/tenants.fixture.ts @@ -0,0 +1,4 @@ + +import { tenant } from '^/domain/tenant'; + +export const TENANTS = { default: tenant }; diff --git a/test/domain/relation/getFollowersAggregated.spec.ts b/test/domain/relation/getFollowersAggregated.spec.ts index 07344572..fa1d16fb 100644 --- a/test/domain/relation/getFollowersAggregated.spec.ts +++ b/test/domain/relation/getFollowersAggregated.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import getFollowers from '^/domain/relation/getFollowersAggregated'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -14,7 +14,7 @@ describe('domain/relation/getFollowers', () => { it('should retrieve follower relations for a following creator', async () => { - const relations = await getFollowers(REQUESTERS.FIRST, VALUES.IDS.CREATOR3, VALUES.RANGE); + const relations = await getFollowers(TENANTS.default, REQUESTERS.FIRST, VALUES.IDS.CREATOR3, VALUES.RANGE); expect(relations).toHaveLength(2); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR1); expect(relations[1].following?.id).toBe(VALUES.IDS.CREATOR2); diff --git a/test/domain/relation/getFollowingAggregated.spec.ts b/test/domain/relation/getFollowingAggregated.spec.ts index 3000e943..b50187e4 100644 --- a/test/domain/relation/getFollowingAggregated.spec.ts +++ b/test/domain/relation/getFollowingAggregated.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import getFollowing from '^/domain/relation/getFollowingAggregated'; -import { DATABASES, REQUESTERS, VALUES } from './fixtures'; +import { DATABASES, REQUESTERS, TENANTS, VALUES } from './fixtures'; beforeEach(async () => { @@ -14,7 +14,7 @@ describe('domain/relation/getFollowing', () => { it('should retrieve relations for a follower', async () => { - const relations = await getFollowing(REQUESTERS.FIRST, VALUES.IDS.CREATOR1, VALUES.RANGE); + const relations = await getFollowing(TENANTS.default, REQUESTERS.FIRST, VALUES.IDS.CREATOR1, VALUES.RANGE); expect(relations).toHaveLength(2); expect(relations[0].following?.id).toBe(VALUES.IDS.CREATOR2); expect(relations[1].following?.id).toBe(VALUES.IDS.CREATOR3); diff --git a/test/domain/tenant/fixtures/databases.fixtures.ts b/test/domain/tenant/fixtures/databases.fixtures.ts new file mode 100644 index 00000000..6b63eef8 --- /dev/null +++ b/test/domain/tenant/fixtures/databases.fixtures.ts @@ -0,0 +1,21 @@ + +import database from '^/integrations/database'; + +import { RECORD_TYPE as TENANT_RECORD_TYPE } from '^/domain/tenant'; + +import { RECORDS } from './records.fixtures'; + +await database.connect(); + +async function tenants(): Promise +{ + await database.clear(); + + const promises = [ + RECORDS.TENANTS.map(tenant => database.createRecord(TENANT_RECORD_TYPE, { ...tenant })) + ]; + + await Promise.all(promises.flat()); +} + +export const DATABASES = { tenants }; diff --git a/test/domain/tenant/fixtures/index.ts b/test/domain/tenant/fixtures/index.ts new file mode 100644 index 00000000..715d332e --- /dev/null +++ b/test/domain/tenant/fixtures/index.ts @@ -0,0 +1,3 @@ + +export * from './databases.fixtures'; +export * from './values.fixtures'; diff --git a/test/domain/tenant/fixtures/records.fixtures.ts b/test/domain/tenant/fixtures/records.fixtures.ts new file mode 100644 index 00000000..c47ff262 --- /dev/null +++ b/test/domain/tenant/fixtures/records.fixtures.ts @@ -0,0 +1,12 @@ + +import type { RecordData } from '^/integrations/database'; + +import type { DataModel as TenantDataModel } from '^/domain/tenant'; + +import { VALUES } from './values.fixtures'; + +export const TENANTS: TenantDataModel[] = [ + { id: VALUES.IDS.TENANT1, origins: [VALUES.ORIGINS.FIRST, VALUES.ORIGINS.SECOND] } +]; + +export const RECORDS: Record = { TENANTS }; diff --git a/test/domain/tenant/fixtures/values.fixtures.ts b/test/domain/tenant/fixtures/values.fixtures.ts new file mode 100644 index 00000000..120926d9 --- /dev/null +++ b/test/domain/tenant/fixtures/values.fixtures.ts @@ -0,0 +1,12 @@ + +export const VALUES = +{ + IDS: { + TENANT1: 'example.com' + }, + ORIGINS: { + FIRST: 'http://alpha.example.com', + SECOND: 'http://beta.example.com', + UNKNOWN: 'unknown' + } +}; diff --git a/test/domain/tenant/getByOrigin.spec.ts b/test/domain/tenant/getByOrigin.spec.ts new file mode 100644 index 00000000..b71ffbba --- /dev/null +++ b/test/domain/tenant/getByOrigin.spec.ts @@ -0,0 +1,21 @@ + +import { beforeEach, describe, expect, it } from 'vitest'; + +import getByOrigin, { TenantNotFound } from '^/domain/tenant/getByOrigin'; + +import { DATABASES, VALUES } from './fixtures'; + +beforeEach(async () => +{ + await DATABASES.tenants(); +}); + +describe('domain/tenant/getByOrigin', () => +{ + it('Should reject an invalid origin', async () => + { + const promise = getByOrigin(VALUES.ORIGINS.UNKNOWN); + + await expect(promise).rejects.toThrow(TenantNotFound); + }); +}); diff --git a/test/domain/tenant/getByOriginConverted.spec.ts b/test/domain/tenant/getByOriginConverted.spec.ts new file mode 100644 index 00000000..7e9cde07 --- /dev/null +++ b/test/domain/tenant/getByOriginConverted.spec.ts @@ -0,0 +1,22 @@ + +import { beforeEach, describe, expect, it } from 'vitest'; + +import getByOriginConverted from '^/domain/tenant/getByOriginConverted'; + +import { DATABASES, VALUES } from './fixtures'; + +beforeEach(async () => +{ + await DATABASES.tenants(); +}); + +describe('domain/tenant/getByOriginConverted', () => +{ + it('Should return a multi-origin tenant identified by a single origin', async () => + { + const tenant = await getByOriginConverted(VALUES.ORIGINS.FIRST); + + expect(tenant.id).toEqual(VALUES.IDS.TENANT1); + expect(tenant.origin).toEqual(VALUES.ORIGINS.FIRST); + }); +}); diff --git a/test/integrations/database/implementation.spec.ts b/test/integrations/database/implementation.spec.ts index 3286687a..66b1a647 100644 --- a/test/integrations/database/implementation.spec.ts +++ b/test/integrations/database/implementation.spec.ts @@ -229,9 +229,9 @@ describe('integrations/database/implementation', () => { const data: RecordData = VALUES.NO_MATCH_SIZE; - // This should not throw an error - await expect(database.updateRecords(RECORD_TYPES.PIZZAS, QUERIES.NO_MATCH, data)) - .resolves.toBeUndefined(); + const promise = database.updateRecords(RECORD_TYPES.PIZZAS, QUERIES.NO_MATCH, data); + + await expect(promise).resolves.toBeUndefined(); }); }); diff --git a/vite.config.ts b/vite.config.ts index 075552a6..386d22b8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,9 @@ import tsconfigPaths from 'vite-tsconfig-paths'; const JITAR_URL = 'http://localhost:3000'; const JITAR_SEGMENTS = []; -const JITAR_MIDDLEWARES = ['./integrations/runtime/requesterMiddleware']; +const JITAR_MIDDLEWARES = [ + './integrations/runtime/requesterMiddleware' +]; const jitarConfig: JitarConfig = { sourceDir: 'src',