From 5b06afcfb1af62a9bc57b3e78b9a6a3d46b09403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= Date: Tue, 17 Sep 2024 15:42:51 +0200 Subject: [PATCH] feat(ioc): awilix POC --- .vscode/settings.json | 3 + src/iocContainer.ts | 19 ++ src/repositories.ts | 328 ++++++++++++++--------------- src/resolvers.ts | 192 +++++++++-------- src/services/authz.ts | 32 +-- src/services/comments.ts | 30 +-- src/services/databaseManagement.ts | 210 +++++++++--------- src/services/resources.ts | 59 +++--- tests/integration/regions.spec.ts | 29 --- 9 files changed, 452 insertions(+), 450 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/iocContainer.ts delete mode 100644 tests/integration/regions.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ea7652a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["awilix"] +} diff --git a/src/iocContainer.ts b/src/iocContainer.ts new file mode 100644 index 0000000..184c22b --- /dev/null +++ b/src/iocContainer.ts @@ -0,0 +1,19 @@ +import awilix from 'awilix' +import { saveResourceAclFactory, saveUserFactory } from './repositories' +import { Knex } from 'knex' +import { getMainDbClient } from './services/databaseManagement' + +export const container = awilix.createContainer({ + strict: true, + injectionMode: awilix.InjectionMode.PROXY +}) + +container.register({ + db: awilix.asFunction(getMainDbClient).singleton(), + saveResource: awilix + .asFunction((regionDb: Knex) => saveUserFactory({ db: regionDb })) + .scoped(), + saveResourceAcl: awilix.asFunction(saveResourceAclFactory).scoped() +}) + +container.resolve('saveResource') diff --git a/src/repositories.ts b/src/repositories.ts index e83d234..34100e2 100644 --- a/src/repositories.ts +++ b/src/repositories.ts @@ -1,4 +1,4 @@ -import { Knex } from "knex"; +import { Knex } from 'knex' import { UserRecord, Resource, @@ -9,254 +9,254 @@ import { Organization, OrganizationAcl, OrganizationResourceAcl, - ResourceRegion, -} from "./types"; + ResourceRegion +} from './types' export const saveResourceFactory = ({ db }: { db: Knex }) => - async (resource: Resource): Promise => { - await db("resources").insert(resource); - }; + async (resource: Resource): Promise => { + await db('resources').insert(resource) + } export const findResourceFactory = ({ db }: { db: Knex }) => - async (resourceId: string): Promise => { - return ( - (await db("resources").where({ id: resourceId }).first()) ?? + async (resourceId: string): Promise => { + return ( + (await db('resources').where({ id: resourceId }).first()) ?? null - ); - }; + ) + } export const saveCommentFactory = ({ db }: { db: Knex }) => - async (comment: Comment): Promise => { - await db("comments").insert(comment); - }; + async (comment: Comment): Promise => { + await db('comments').insert(comment) + } export const countCommentsFactory = ({ db }: { db: Knex }) => - async (resourceId: string): Promise => { - const [rawCount] = await db("comments") - .count() - .where({ resourceId }); - return parseInt(rawCount.count as string); - }; + async (resourceId: string): Promise => { + const [rawCount] = await db('comments') + .count() + .where({ resourceId }) + return parseInt(rawCount.count as string) + } export const findUserFactory = ({ db }: { db: Knex }) => - async (userId: string): Promise => { - return ( - (await db("users").where("id", "=", userId).first()) ?? null - ); - }; + async (userId: string): Promise => { + return ( + (await db('users').where('id', '=', userId).first()) ?? null + ) + } export const queryUsersFactoy = ({ db }: { db: Knex }) => - async (): Promise => { - return await db("users").select(); - }; + async (): Promise => { + return await db('users').select() + } export const saveUserFactory = ({ db }: { db: Knex }) => - async (user: UserRecord): Promise => { - await db("users").insert(user); - }; + async (user: UserRecord): Promise => { + await db('users').insert(user) + } export const getUsersResourceAclFactory = ({ db }: { db: Knex }) => - async ({ resourceId, userId }: ResourceAcl): Promise => { - return ( - (await db("resource_acl") - .where({ userId, resourceId }) - .first()) ?? null - ); - }; + async ({ resourceId, userId }: ResourceAcl): Promise => { + return ( + (await db('resource_acl') + .where({ userId, resourceId }) + .first()) ?? null + ) + } export const saveResourceAclFactory = ({ db }: { db: Knex }) => - async (resourceAcl: ResourceAcl): Promise => { - await db("resource_acl").insert(resourceAcl); - }; + async (resourceAcl: ResourceAcl): Promise => { + await db('resource_acl').insert(resourceAcl) + } export const countUsersResourcesFactory = ({ db }: { db: Knex }) => - async (userId: string): Promise => { - const [rawCount] = await db("resource_acl") - .count() - .where({ userId }); - return parseInt(rawCount.count as string); - }; + async (userId: string): Promise => { + const [rawCount] = await db('resource_acl') + .count() + .where({ userId }) + return parseInt(rawCount.count as string) + } export const findUsersResourceFactory = ({ db }: { db: Knex }) => - async ({ resourceId, userId }: ResourceAcl): Promise => { - return ( - (await db("resource_acl") - .where({ userId, resourceId }) - .first()) ?? null - ); - }; + async ({ resourceId, userId }: ResourceAcl): Promise => { + return ( + (await db('resource_acl') + .where({ userId, resourceId }) + .first()) ?? null + ) + } export const queryResourcesFactory = ({ db }: { db: Knex }) => - async ({ - userId, - limit, - cursor, - }: { - userId: string; - limit: number; - cursor: string | null; - }): Promise => { - let query = db("resources") - .join("resource_acl", "resources.id", "resource_acl.resourceId") - .where({ userId }); - if (cursor !== null) { - query = query.andWhere("createdAt", "<", cursor); + async ({ + userId, + limit, + cursor + }: { + userId: string + limit: number + cursor: string | null + }): Promise => { + let query = db('resources') + .join('resource_acl', 'resources.id', 'resource_acl.resourceId') + .where({ userId }) + if (cursor !== null) { + query = query.andWhere('createdAt', '<', cursor) + } + const items = await query.orderBy('createdAt', 'desc').limit(limit) + return items } - const items = await query.orderBy("createdAt", "desc").limit(limit); - return items; - }; export const countResourceCommentsFactory = ({ db }: { db: Knex }) => - async (resourceId: string): Promise => { - const [rawCount] = await db("comments") - .count() - .where({ resourceId }); - return parseInt(rawCount.count as string); - }; + async (resourceId: string): Promise => { + const [rawCount] = await db('comments') + .count() + .where({ resourceId }) + return parseInt(rawCount.count as string) + } export const queryCommentsFactory = ({ db }: { db: Knex }) => - async ({ - resourceId, - limit, - cursor, - }: { - resourceId: string; - limit: number; - cursor: string | null; - }): Promise => { - let query = db("comments").where({ resourceId }); - if (cursor !== null) { - query = query.andWhere("createdAt", "<", cursor); + async ({ + resourceId, + limit, + cursor + }: { + resourceId: string + limit: number + cursor: string | null + }): Promise => { + let query = db('comments').where({ resourceId }) + if (cursor !== null) { + query = query.andWhere('createdAt', '<', cursor) + } + return await query.orderBy('createdAt', 'desc').limit(limit) } - return await query.orderBy("createdAt", "desc").limit(limit); - }; export const queryRegionsFactory = ({ db }: { db: Knex }) => - async ( - params: + async ( + params: | { - connectionString?: string | undefined; - } - | undefined = undefined, - ): Promise => { - let query = db("regions"); - if (params?.connectionString !== undefined) query = query.where(params); - return await query.select(); - }; + connectionString?: string | undefined + } + | undefined = undefined + ): Promise => { + let query = db('regions') + if (params?.connectionString !== undefined) query = query.where(params) + return await query.select() + } export const findRegionFactory = ({ db }: { db: Knex }) => - async (id: string): Promise => { - return (await db("regions").where({ id }).first()) ?? null; - }; + async (id: string): Promise => { + return (await db('regions').where({ id }).first()) ?? null + } export const queryOrganizationsRegionsFactory = ({ db }: { db: Knex }) => - async (): Promise => { - return await db("organizations_regions").select(); - }; + async (): Promise => { + return await db('organizations_regions').select() + } export const findOrganizationRegionFactory = ({ db }: { db: Knex }) => - async ({ - regionId, - organizationId, - }: OrganizationsRegions): Promise => { - return ( - (await db("organizations_regions") - .where({ regionId, organizationId }) - .first()) ?? null - ); - }; + async ({ + regionId, + organizationId + }: OrganizationsRegions): Promise => { + return ( + (await db('organizations_regions') + .where({ regionId, organizationId }) + .first()) ?? null + ) + } export const saveRegionFactory = ({ db }: { db: Knex }) => - async (region: Region): Promise => { - await db("regions").insert(region); - }; + async (region: Region): Promise => { + await db('regions').insert(region) + } export const saveOrganizationFactory = ({ db }: { db: Knex }) => - async (organization: Organization): Promise => { - await db("organizations").insert(organization); - }; + async (organization: Organization): Promise => { + await db('organizations').insert(organization) + } export const findOrganizationFactory = ({ db }: { db: Knex }) => - async (id: string): Promise => { - return ( - (await db("organizations").where({ id }).first()) ?? null - ); - }; + async (id: string): Promise => { + return ( + (await db('organizations').where({ id }).first()) ?? null + ) + } export const queryOrganizationsFactory = ({ db }: { db: Knex }) => - async (): Promise => { - return await db("organizations").select(); - }; + async (): Promise => { + return await db('organizations').select() + } export const saveOrganizationRegionFactory = ({ db }: { db: Knex }) => - async (or: OrganizationsRegions): Promise => { - return await db("organizations_regions").insert(or); - }; + async (or: OrganizationsRegions): Promise => { + return await db('organizations_regions').insert(or) + } export const saveOrganizationAclFactory = ({ db }: { db: Knex }) => - async (orgAcl: OrganizationAcl): Promise => { - await db("organization_acl").insert(orgAcl); - }; + async (orgAcl: OrganizationAcl): Promise => { + await db('organization_acl').insert(orgAcl) + } export const findOrganizationAclFactory = ({ db }: { db: Knex }) => - async ({ - userId, - organizationId, - }: OrganizationAcl): Promise => { - return ( - (await db("organization_acl") - .where({ userId, organizationId }) - .first()) ?? null - ); - }; + async ({ + userId, + organizationId + }: OrganizationAcl): Promise => { + return ( + (await db('organization_acl') + .where({ userId, organizationId }) + .first()) ?? null + ) + } export const saveOrganizationResourceAclFactory = ({ db }: { db: Knex }) => - async (item: OrganizationResourceAcl): Promise => { - await db("organization_resource_acl").insert(item); - }; + async (item: OrganizationResourceAcl): Promise => { + await db('organization_resource_acl').insert(item) + } export const findResourceRegionFactory = ({ db }: { db: Knex }) => - async ({ - resourceId, - }: { - resourceId: string; - }): Promise => { - return ( - (await db("resource_region") - .where({ resourceId }) - .first()) ?? null - ); - }; + async ({ + resourceId + }: { + resourceId: string + }): Promise => { + return ( + (await db('resource_region') + .where({ resourceId }) + .first()) ?? null + ) + } export const saveResourceRegionFactory = ({ db }: { db: Knex }) => - async (item: ResourceRegion): Promise => { - await db("resource_region").insert(item); - }; + async (item: ResourceRegion): Promise => { + await db('resource_region').insert(item) + } diff --git a/src/resolvers.ts b/src/resolvers.ts index cdcc96f..47639b0 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -1,9 +1,10 @@ -import { getCommentsFactory } from "./services/comments"; +import { getCommentsFactory } from './services/comments' +import awilix from 'awilix' import { createResourceFactory, - getResourcesFactory, -} from "./services/resources"; -import { GraphQLError } from "graphql"; + getResourcesFactory +} from './services/resources' +import { GraphQLError } from 'graphql' import { Resource, UserRecord, @@ -13,17 +14,17 @@ import { OrganizationsRegions, OrganizationAcl, CommentCreateArgs, - UserCreateArgs, -} from "./types"; + UserCreateArgs +} from './types' import { createOrganization, registerRegion, getResourceDb, getMainDbClient, - getRegionDb, -} from "./services/databaseManagement"; -import { authorizeUserOrgRegionFactory } from "./services/authz"; -import cryptoRandomString from "crypto-random-string"; + getRegionDb +} from './services/databaseManagement' +import { authorizeUserOrgRegionFactory } from './services/authz' +import cryptoRandomString from 'crypto-random-string' import { countCommentsFactory, countUsersResourcesFactory, @@ -42,162 +43,167 @@ import { saveOrganizationRegionFactory, saveOrganizationResourceAclFactory, saveResourceAclFactory, - saveResourceFactory, saveResourceRegionFactory, - saveUserFactory, -} from "./repositories"; + saveUserFactory +} from './repositories' +import { container } from './iocContainer' -const db = getMainDbClient(); +const db = getMainDbClient() // Resolvers define how to fetch the types defined in your schema. // This resolver retrieves books from the "books" array above. export const resolvers = { Query: { - async users() { - return await queryUsersFactoy({ db })(); + async users () { + return await queryUsersFactoy({ db })() }, - async user(_: unknown, args: { id: string }) { - return await findUserFactory({ db })(args.id); + async user (_: unknown, args: { id: string }) { + return await findUserFactory({ db })(args.id) }, - async resource( + async resource ( _: unknown, - args: { id: string; userId: string }, + args: { id: string, userId: string } ): Promise { const maybeAcl = await getUsersResourceAclFactory({ db })({ userId: args.userId, - resourceId: args.id, - }); + resourceId: args.id + }) if (maybeAcl == null) { throw new GraphQLError( "The user doesn't have access to the given resource", { extensions: { - code: "FORBIDDEN", - }, - }, - ); + code: 'FORBIDDEN' + } + } + ) } - const resourceDb = await getResourceDb(args.id); + const resourceDb = await getResourceDb(args.id) const maybeResource = await findResourceFactory({ db: resourceDb })( - args.id, - ); + args.id + ) if (maybeResource == null) { - throw new GraphQLError("Resource not found", { - extensions: { code: "RESOURCE_NOT_FOUND" }, - }); + throw new GraphQLError('Resource not found', { + extensions: { code: 'RESOURCE_NOT_FOUND' } + }) } - return maybeResource; + return maybeResource }, - async organizations() { - return await queryOrganizationsFactory({ db })(); - }, - async regions() { - return await queryRegionsFactory({ db })(); + async organizations () { + return await queryOrganizationsFactory({ db })() }, + async regions () { + return await queryRegionsFactory({ db })() + } }, User: { - async resources(parent: UserRecord, args: PaginationArgs) { + async resources (parent: UserRecord, args: PaginationArgs) { return await getResourcesFactory( countUsersResourcesFactory({ db }), - queryResourcesFactory({ db }), - )({ userId: parent.id, ...args }); - }, + queryResourcesFactory({ db }) + )({ userId: parent.id, ...args }) + } }, Resource: { - async comments( + async comments ( parent: Resource, - { limit, cursor }: PaginationArgs, + { limit, cursor }: PaginationArgs ): Promise { - const resourceDb = await getResourceDb(parent.id); + const resourceDb = await getResourceDb(parent.id) return await getCommentsFactory( countCommentsFactory({ db: resourceDb }), - queryCommentsFactory({ db: resourceDb }), + queryCommentsFactory({ db: resourceDb }) )({ resourceId: parent.id, limit, - cursor, - }); - }, + cursor + }) + } }, Mutation: { - async createUser( + async createUser ( _: unknown, - { input: { name } }: { input: UserCreateArgs }, + { input: { name } }: { input: UserCreateArgs } ) { - const id = cryptoRandomString({ length: 10 }); - await saveUserFactory({ db })({ id, name }); - return id; + const id = cryptoRandomString({ length: 10 }) + await saveUserFactory({ db })({ id, name }) + return id }, - async registerRegion( + async registerRegion ( _: unknown, args: { - name: string; - connectionString: string; - sslCaCert: string | null; - }, + name: string + connectionString: string + sslCaCert: string | null + } ) { - return await registerRegion(args); + return await registerRegion(args) }, - async createOrganization(_: unknown, args: { name: string }) { - return await createOrganization(args.name); + async createOrganization (_: unknown, args: { name: string }) { + return await createOrganization(args.name) }, - async addRegionToOrganization(_: unknown, args: OrganizationsRegions) { - await saveOrganizationRegionFactory({ db })(args); + async addRegionToOrganization (_: unknown, args: OrganizationsRegions) { + await saveOrganizationRegionFactory({ db })(args) }, - async addUserToOrganization( + async addUserToOrganization ( _: unknown, - { input: args }: { input: OrganizationAcl }, + { input: args }: { input: OrganizationAcl } ) { - await saveOrganizationAclFactory({ db })(args); + await saveOrganizationAclFactory({ db })(args) }, - async createResource( + async createResource ( _: unknown, - { input: args }: { input: ResourceCreateArgs }, + { input: args }: { input: ResourceCreateArgs } ) { await authorizeUserOrgRegionFactory( findOrganizationAclFactory({ db }), - findOrganizationRegionFactory({ db }), - )(args); + findOrganizationRegionFactory({ db }) + )(args) const resourceDb = args.regionId !== null ? await getRegionDb({ regionId: args.regionId }) - : db; + : db - const resourceId = await createResourceFactory( - saveResourceFactory({ db: resourceDb }), - saveResourceAclFactory({ db }), - )(args); + const requestContainer = container.createScope() + requestContainer.register({ resourceDb: awilix.asValue(resourceDb) }) + + const saveResource = requestContainer.resolve('saveResource') + + const resourceId = await createResourceFactory({ + saveResource, + saveResourceAcl: saveResourceAclFactory({ db }) + })(args) if (args.organizationId !== null) { await saveOrganizationResourceAclFactory({ db })({ organizationId: args.organizationId, - resourceId, - }); + resourceId + }) if (args.regionId !== null) { await saveResourceRegionFactory({ db })({ resourceId, // i know its not null here, the authz function ensures it - regionId: args.regionId, - }); + regionId: args.regionId + }) } } - return resourceId; + return resourceId }, - async addComment( + async addComment ( _: unknown, - { input: args }: { input: CommentCreateArgs }, + { input: args }: { input: CommentCreateArgs } ) { - const resourceAcl = await getUsersResourceAclFactory({ db })(args); + const resourceAcl = await getUsersResourceAclFactory({ db })(args) if (resourceAcl == null) { - throw new Error("The user doesn't have access to the given resource"); + throw new Error("The user doesn't have access to the given resource") } // 2. get resource db client - const resourceDb = await getResourceDb(args.resourceId); + const resourceDb = await getResourceDb(args.resourceId) // 3. save comment to db - const id = cryptoRandomString({ length: 10 }); - const createdAt = new Date(); - await saveCommentFactory({ db: resourceDb })({ id, createdAt, ...args }); - return id; - }, - }, -}; + const id = cryptoRandomString({ length: 10 }) + const createdAt = new Date() + await saveCommentFactory({ db: resourceDb })({ id, createdAt, ...args }) + return id + } + } +} diff --git a/src/services/authz.ts b/src/services/authz.ts index b4d43e2..6b213a4 100644 --- a/src/services/authz.ts +++ b/src/services/authz.ts @@ -1,29 +1,29 @@ import { OrganizationAcl, OrganizationsRegions, - UserOrgRegionArgs, -} from "../types"; + UserOrgRegionArgs +} from '../types' export const authorizeUserOrgRegionFactory = ( orgAclGetter: (params: OrganizationAcl) => Promise, orgRegionGetter: ( params: OrganizationsRegions, - ) => Promise, + ) => Promise ) => - async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => { - if (!organizationId && regionId) { - throw new Error("public org doesn't support regions"); - } - if (organizationId) { - if (!regionId) throw new Error("organizations can only write to regions"); - const orgAcl = await orgAclGetter({ organizationId, userId }); - if (orgAcl == null) { - throw new Error("user doesn't have access to this organization"); + async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => { + if (!organizationId && regionId) { + throw new Error("public org doesn't support regions") } - const orgRegion = await orgRegionGetter({ organizationId, regionId }); - if (orgRegion == null) { - throw new Error("organization doesnt have access to this region"); + if (organizationId) { + if (!regionId) throw new Error('organizations can only write to regions') + const orgAcl = await orgAclGetter({ organizationId, userId }) + if (orgAcl == null) { + throw new Error("user doesn't have access to this organization") + } + const orgRegion = await orgRegionGetter({ organizationId, regionId }) + if (orgRegion == null) { + throw new Error('organization doesnt have access to this region') + } } } - }; diff --git a/src/services/comments.ts b/src/services/comments.ts index 6a37652..f00e76a 100644 --- a/src/services/comments.ts +++ b/src/services/comments.ts @@ -1,25 +1,25 @@ -import { CommentCollection, PaginationArgs, Comment } from "../types"; +import { CommentCollection, PaginationArgs, Comment } from '../types' interface GetCommentsArgs extends PaginationArgs { - resourceId: string; + resourceId: string } export const getCommentsFactory = ( countComments: (resourceId: string) => Promise, - queryComments: (params: GetCommentsArgs) => Promise, + queryComments: (params: GetCommentsArgs) => Promise ) => - async (params: GetCommentsArgs): Promise => { + async (params: GetCommentsArgs): Promise => { // yes, i should be doing base64 de and encoding with the cursor... - const totalCount = await countComments(params.resourceId); - const items = await queryComments(params); - let cursor = null; - if (items.length > 0) { - cursor = items.slice(-1)[0].createdAt.toISOString(); + const totalCount = await countComments(params.resourceId) + const items = await queryComments(params) + let cursor = null + if (items.length > 0) { + cursor = items.slice(-1)[0].createdAt.toISOString() + } + return { + totalCount, + items, + cursor + } } - return { - totalCount, - items, - cursor, - }; - }; diff --git a/src/services/databaseManagement.ts b/src/services/databaseManagement.ts index 7d28fd4..fedf8a0 100644 --- a/src/services/databaseManagement.ts +++ b/src/services/databaseManagement.ts @@ -1,162 +1,162 @@ -import { POSTGRES_URL } from "../config"; -import knex, { Knex } from "knex"; -import cryptoRandomString from "crypto-random-string"; +import { POSTGRES_URL } from '../config' +import knex, { Knex } from 'knex' +import cryptoRandomString from 'crypto-random-string' import { findRegionFactory, findResourceRegionFactory, queryRegionsFactory, saveOrganizationFactory, - saveRegionFactory, -} from "../repositories"; + saveRegionFactory +} from '../repositories' const migrateToLatest = async (db: Knex): Promise => { const plannedMigrations: Array<{ file: string }> = ( await db.migrate.list() - )[1]; + )[1] if (plannedMigrations.length > 0) { console.log( `🕰️ planning migrations: ${plannedMigrations .map((m) => m.file) - .join(",")}`, - ); + .join(',')}` + ) } else { - console.log("no migrations are planned"); + console.log('no migrations are planned') } // TODO: make sure if a migration fails, all migrations are rolled back - await db.migrate.latest(); -}; + await db.migrate.latest() +} export const migrateAll = async (): Promise => { - await migrateToLatest(db); - const dbClients = await getAllDbClients(); + await migrateToLatest(db) + const dbClients = await getAllDbClients() await Promise.all([ - ...dbClients.map(async (db) => await migrateToLatest(db)), - ]); -}; + ...dbClients.map(async (db) => await migrateToLatest(db)) + ]) +} const createDatabaseConfig = ( connectionString: string, - sslCaCert: string | null, + sslCaCert: string | null ): Knex.Config => { const config: Knex.Config = { - client: "pg", + client: 'pg', connection: { connectionString, ssl: sslCaCert ? { ca: sslCaCert, - rejectUnauthorized: true, + rejectUnauthorized: true } - : undefined, + : undefined }, migrations: { - directory: "src/migrations", - extension: "ts", - }, - }; - return config; -}; + directory: 'src/migrations', + extension: 'ts' + } + } + return config +} -const db = knex(createDatabaseConfig(POSTGRES_URL, null)); +const db = knex(createDatabaseConfig(POSTGRES_URL, null)) -const dbClientStore: Map = new Map(); +const dbClientStore: Map = new Map() -const findRegion = findRegionFactory({ db }); +const findRegion = findRegionFactory({ db }) export const getRegionDb = async ({ - regionId, + regionId }: { - regionId: string | undefined; + regionId: string | undefined }): Promise => { - if (!regionId) return db; - const maybeClient = dbClientStore.get(regionId); - if (maybeClient != null) return maybeClient; - const maybeRegion = await findRegion(regionId); - if (maybeRegion == null) throw Error(`region ${regionId} not found`); + if (!regionId) return db + const maybeClient = dbClientStore.get(regionId) + if (maybeClient != null) return maybeClient + const maybeRegion = await findRegion(regionId) + if (maybeRegion == null) throw Error(`region ${regionId} not found`) const client = knex( - createDatabaseConfig(maybeRegion.connectionString, maybeRegion.sslCaCert), - ); - dbClientStore.set(regionId, client); - return client; -}; + createDatabaseConfig(maybeRegion.connectionString, maybeRegion.sslCaCert) + ) + dbClientStore.set(regionId, client) + return client +} -export const getMainDbClient = (): Knex => db; +export const getMainDbClient = (): Knex => db -const queryRegions = queryRegionsFactory({ db }); -const saveRegion = saveRegionFactory({ db }); +const queryRegions = queryRegionsFactory({ db }) +const saveRegion = saveRegionFactory({ db }) export const registerRegion = async ({ name, connectionString, - sslCaCert, + sslCaCert }: { - name: string; - connectionString: string; - sslCaCert: string | null; + name: string + connectionString: string + sslCaCert: string | null }): Promise => { - const regions = await queryRegions({ connectionString }); - if (regions.length > 0) throw new Error("This region is already registered"); - const id = cryptoRandomString({ length: 10 }); - const newDb = knex(createDatabaseConfig(connectionString, sslCaCert)); - await migrateToLatest(newDb); - dbClientStore.set(id, newDb); + const regions = await queryRegions({ connectionString }) + if (regions.length > 0) throw new Error('This region is already registered') + const id = cryptoRandomString({ length: 10 }) + const newDb = knex(createDatabaseConfig(connectionString, sslCaCert)) + await migrateToLatest(newDb) + dbClientStore.set(id, newDb) - const sslmode = sslCaCert ? "require" : "disable"; + const sslmode = sslCaCert ? 'require' : 'disable' await setUpUserReplication({ from: db, to: newDb, regionName: name, - sslmode, - }); + sslmode + }) await setUpResourceReplication({ from: newDb, to: db, regionName: name, - sslmode, - }); + sslmode + }) await saveRegion({ id, name, connectionString, - sslCaCert, - }); - return id; -}; + sslCaCert + }) + return id +} -const saveOrganization = saveOrganizationFactory({ db }); +const saveOrganization = saveOrganizationFactory({ db }) export const createOrganization = async (name: string): Promise => { - const id = cryptoRandomString({ length: 10 }); - await saveOrganization({ id, name }); - return id; -}; + const id = cryptoRandomString({ length: 10 }) + await saveOrganization({ id, name }) + return id +} interface ReplicationArgs { - from: Knex; - to: Knex; - sslmode: string; - regionName: string; + from: Knex + to: Knex + sslmode: string + regionName: string } const setUpUserReplication = async ({ from, to, sslmode, - regionName, + regionName }: ReplicationArgs): Promise => { // TODO: ensure its created... try { - await from.raw("CREATE PUBLICATION userspub FOR TABLE users;"); + await from.raw('CREATE PUBLICATION userspub FOR TABLE users;') } catch (err) { - if (!(err instanceof Error)) throw err; - if (!err.message.includes("already exists")) throw err; + if (!(err instanceof Error)) throw err + if (!err.message.includes('already exists')) throw err } - const fromUrl = new URL(from.client.config.connection.connectionString); - const fromDbName = fromUrl.pathname.replace("/", ""); - const subName = `userssub_${regionName}`; + const fromUrl = new URL(from.client.config.connection.connectionString) + const fromDbName = fromUrl.pathname.replace('/', '') + const subName = `userssub_${regionName}` const rawSqeel = `SELECT * FROM aiven_extras.pg_create_subscription( '${subName}', 'dbname=${fromDbName} host=${fromUrl.hostname} port=${fromUrl.port} sslmode=${sslmode} user=${fromUrl.username} password=${fromUrl.password}', @@ -164,32 +164,32 @@ const setUpUserReplication = async ({ '${subName}', TRUE, TRUE - );`; + );` try { - await to.raw(rawSqeel); + await to.raw(rawSqeel) } catch (err) { - if (!(err instanceof Error)) throw err; - if (!err.message.includes("already exists")) throw err; + if (!(err instanceof Error)) throw err + if (!err.message.includes('already exists')) throw err } -}; +} const setUpResourceReplication = async ({ from, to, regionName, - sslmode, + sslmode }: ReplicationArgs): Promise => { // TODO: ensure its created... try { - await from.raw("CREATE PUBLICATION resourcepub FOR TABLE resources;"); + await from.raw('CREATE PUBLICATION resourcepub FOR TABLE resources;') } catch (err) { - if (!(err instanceof Error)) throw err; - if (!err.message.includes("already exists")) throw err; + if (!(err instanceof Error)) throw err + if (!err.message.includes('already exists')) throw err } - const fromUrl = new URL(from.client.config.connection.connectionString); - const fromDbName = fromUrl.pathname.replace("/", ""); - const subName = `resourcesub_${regionName}`; + const fromUrl = new URL(from.client.config.connection.connectionString) + const fromDbName = fromUrl.pathname.replace('/', '') + const subName = `resourcesub_${regionName}` const rawSqeel = `SELECT * FROM aiven_extras.pg_create_subscription( '${subName}', 'dbname=${fromDbName} host=${fromUrl.hostname} port=${fromUrl.port} sslmode=${sslmode} user=${fromUrl.username} password=${fromUrl.password}', @@ -197,26 +197,26 @@ const setUpResourceReplication = async ({ '${subName}', TRUE, TRUE - );`; + );` try { - await to.raw(rawSqeel); + await to.raw(rawSqeel) } catch (err) { - if (!(err instanceof Error)) throw err; - if (!err.message.includes("already exists")) throw err; + if (!(err instanceof Error)) throw err + if (!err.message.includes('already exists')) throw err } -}; +} export const getAllDbClients = async (): Promise => { - const regions = await queryRegions({}); + const regions = await queryRegions({}) const regionClients = await Promise.all( - regions.map(async (region) => await getRegionDb({ regionId: region.id })), - ); - return [db, ...regionClients]; -}; + regions.map(async (region) => await getRegionDb({ regionId: region.id })) + ) + return [db, ...regionClients] +} -const findResourceRegion = findResourceRegionFactory({ db }); +const findResourceRegion = findResourceRegionFactory({ db }) export const getResourceDb = async (resourceId: string): Promise => { - const resourceRegion = await findResourceRegion({ resourceId }); - return resourceRegion != null ? await getRegionDb(resourceRegion) : db; -}; + const resourceRegion = await findResourceRegion({ resourceId }) + return resourceRegion != null ? await getRegionDb(resourceRegion) : db +} diff --git a/src/services/resources.ts b/src/services/resources.ts index 04abe81..8a4bf4e 100644 --- a/src/services/resources.ts +++ b/src/services/resources.ts @@ -1,47 +1,50 @@ -import cryptoRandomString from "crypto-random-string"; +import cryptoRandomString from 'crypto-random-string' import { Resource, PaginationArgs, ResourceCollection, ResourceCreateArgs, - ResourceAcl, -} from "../types"; + ResourceAcl +} from '../types' interface GetResourcesArgs extends PaginationArgs { - userId: string; + userId: string } export const getResourcesFactory = ( countResources: (userId: string) => Promise, - queryResources: (params: GetResourcesArgs) => Promise, + queryResources: (params: GetResourcesArgs) => Promise ) => - async (params: GetResourcesArgs): Promise => { - const totalCount = await countResources(params.userId); - const items = await queryResources(params); - let cursor = null; - if (items.length > 0) { - cursor = items.slice(-1)[0].createdAt.toISOString(); + async (params: GetResourcesArgs): Promise => { + const totalCount = await countResources(params.userId) + const items = await queryResources(params) + let cursor = null + if (items.length > 0) { + cursor = items.slice(-1)[0].createdAt.toISOString() + } + return { + totalCount, + items, + cursor + } } - return { - totalCount, - items, - cursor, - }; - }; export const createResourceFactory = - ( - resourceSaver: (resource: Resource) => Promise, - resourceAclSaver: (resourceAcl: ResourceAcl) => Promise, - ) => - async ({ userId, name }: ResourceCreateArgs): Promise => { + ({ + saveResource, + saveResourceAcl + }: { + saveResource: (resource: Resource) => Promise + saveResourceAcl: (resourceAcl: ResourceAcl) => Promise + }) => + async ({ userId, name }: ResourceCreateArgs): Promise => { // 1. if no org, create project in main region, validate that, regionId is null // 2. if org, validate if user has access to the org // 3. if org and region, validate if org has access to region // 4. create resource - const id = cryptoRandomString({ length: 10 }); - const resource = { id, name, createdAt: new Date() }; - await resourceSaver(resource); - await resourceAclSaver({ resourceId: id, userId }); - return id; - }; + const id = cryptoRandomString({ length: 10 }) + const resource = { id, name, createdAt: new Date() } + await saveResource(resource) + await saveResourceAcl({ resourceId: id, userId }) + return id + } diff --git a/tests/integration/regions.spec.ts b/tests/integration/regions.spec.ts deleted file mode 100644 index 1c3c4cd..0000000 --- a/tests/integration/regions.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, beforeAll, describe, it } from 'vitest' -import { - getOrganizationRegionsFrom, - getRegionsFrom -} from '../../src/repositories' -import { - getMainDbClient, - migrateAll -} from '../../src/services/databaseManagement' -import { Knex } from 'knex' - -describe('regions', () => { - let dbClient: Knex - - beforeAll(async () => { - dbClient = await getMainDbClient() - }) - it('gets all regions', async () => { - const regions = await getRegionsFrom(dbClient)() - expect(regions.length).toBeGreaterThan(0) - }) - it('gets organizations regions', async () => { - const orgRegions = await getOrganizationRegionsFrom(dbClient)() - expect(orgRegions.length).toBeGreaterThan(0) - }) - it('migrates all', async () => { - await migrateAll() - }) -})