2 Commits

Author SHA1 Message Date
Gergő Jedlicska 5b06afcfb1 feat(ioc): awilix POC 2024-09-17 15:42:51 +02:00
Gergő Jedlicska daea6d3765 add aiven extras to db containers, migrate to new repo pattern 2024-09-11 22:37:24 +02:00
14 changed files with 3675 additions and 2932 deletions
+1
View File
@@ -5,5 +5,6 @@
.swc .swc
node_modules node_modules
ca-cert* ca-cert*
.data/*
dist dist
+3
View File
@@ -0,0 +1,3 @@
{
"cSpell.words": ["awilix"]
}
+25
View File
@@ -0,0 +1,25 @@
FROM postgres:14.5-alpine as builder
RUN apk add --no-cache 'git=~2.36' \
'build-base=~0.5' \
'clang=~13.0' \
'llvm13=~13.0'
WORKDIR /
RUN git clone --branch 1.1.9 https://github.com/aiven/aiven-extras.git aiven-extras
WORKDIR /aiven-extras
RUN git checkout 36598ab \
&& git clean -df \
&& make \
&& make install
FROM postgres:14.5-alpine
COPY --from=builder /aiven-extras/aiven_extras.control /usr/local/share/postgresql/extension/aiven_extras.control
COPY --from=builder /aiven-extras/sql/aiven_extras.sql /usr/local/share/postgresql/extension/aiven_extras--1.1.9.sql
COPY --from=builder /aiven-extras/aiven_extras.so /usr/local/lib/postgresql/aiven_extras.so
EXPOSE 5432
CMD ["postgres"]
+34 -16
View File
@@ -1,35 +1,53 @@
version: "3.9" version: '3.9'
services: services:
postgres: main-db:
image: postgres:16-alpine build:
context: aiven_postgres
dockerfile: Dockerfile
volumes:
- ./.data/main-db:/var/lib/postgresql/data
ports: ports:
- 5454:5432 - 5454:5432
volumes:
- ./.postgres-data:/var/lib/postgresql/data
environment: environment:
- POSTGRES_PASSWORD=speckle - POSTGRES_PASSWORD=speckle
- POSTGRES_USER=speckle - POSTGRES_USER=speckle
- POSTGRES_DB=speckle_main - POSTGRES_DB=speckle
extra_hosts:
- host.docker.internal:host-gateway
region-1: region-1-db:
image: postgres:16-alpine build:
context: aiven_postgres
dockerfile: Dockerfile
volumes:
- ./.data/region-1-db:/var/lib/postgresql/data
ports: ports:
- 5455:5432 - 5455:5432
volumes:
- ./.postgres-region-1:/var/lib/postgresql/data
environment: environment:
- POSTGRES_PASSWORD=speckle - POSTGRES_PASSWORD=speckle
- POSTGRES_USER=speckle - POSTGRES_USER=speckle
- POSTGRES_DB=speckle_main - POSTGRES_DB=speckle
depends_on:
- main-db
region-2: extra_hosts:
image: postgres:16-alpine - host.docker.internal:host-gateway
region-2-db:
build:
context: aiven_postgres
dockerfile: Dockerfile
volumes:
- ./.data/region-2-db:/var/lib/postgresql/data
ports: ports:
- 5456:5432 - 5456:5432
volumes:
- ./.postgres-region-2:/var/lib/postgresql/data
environment: environment:
- POSTGRES_PASSWORD=speckle - POSTGRES_PASSWORD=speckle
- POSTGRES_USER=speckle - POSTGRES_USER=speckle
- POSTGRES_DB=speckle_main - POSTGRES_DB=speckle
depends_on:
- main-db
extra_hosts:
- host.docker.internal:host-gateway
+1
View File
@@ -31,6 +31,7 @@
}, },
"dependencies": { "dependencies": {
"@apollo/server": "^4.10.0", "@apollo/server": "^4.10.0",
"awilix": "^11.0.0",
"crypto-random-string": "^3.0.0", "crypto-random-string": "^3.0.0",
"dataloader": "^2.2.2", "dataloader": "^2.2.2",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
+3202 -2563
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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')
+239 -224
View File
@@ -12,236 +12,251 @@ import {
ResourceRegion ResourceRegion
} from './types' } from './types'
export class RegionRepo { export const saveResourceFactory =
db: Knex ({ db }: { db: Knex }) =>
async (resource: Resource): Promise<void> => {
constructor (db: Knex) { await db<Resource>('resources').insert(resource)
this.db = db
}
async saveResource (resource: Resource): Promise<void> {
await this.db<Resource>('resources').insert(resource)
}
async findResource (resourceId: string): Promise<Resource | null> {
return (
(await this.db<Resource>('resources')
.where({ id: resourceId })
.first()) ?? null
)
}
async saveComment (comment: Comment): Promise<void> {
await this.db<Comment>('comments').insert(comment)
}
async countComments (resourceId: string): Promise<number> {
const [rawCount] = await this.db<Comment>('comments')
.count()
.where({ resourceId })
return parseInt(rawCount.count as string)
}
async queryComments ({
resourceId,
limit,
cursor
}: {
resourceId: string
limit: number
cursor: string | null
}): Promise<Comment[]> {
const query = this.db<Comment>('comments').where({ resourceId })
if (cursor) {
query.andWhere('createdAt', '<', cursor)
} }
return await query.limit(limit)
}
}
export class MainRepo extends RegionRepo { export const findResourceFactory =
async findUser (userId: string): Promise<UserRecord | null> { ({ db }: { db: Knex }) =>
return ( async (resourceId: string): Promise<Resource | null> => {
(await this.db<UserRecord>('users').where('id', '=', userId).first()) ?? return (
(await db<Resource>('resources').where({ id: resourceId }).first()) ??
null null
) )
}
async queryUsers (): Promise<UserRecord[]> {
return await this.db<UserRecord>('users').select()
}
async saveUser (user: UserRecord): Promise<void> {
await this.db<UserRecord>('users').insert(user)
}
async getUsersResourceAcl ({
resourceId,
userId
}: ResourceAcl): Promise<ResourceAcl | null> {
return (
(await this.db<ResourceAcl>('resource_acl')
.where({ userId, resourceId })
.first()) ?? null
)
}
async saveResourceAcl (resourceAcl: ResourceAcl): Promise<void> {
await this.db<ResourceAcl>('resource_acl').insert(resourceAcl)
}
async countUsersResources (userId: string): Promise<number> {
const [rawCount] = await this.db<ResourceAcl>('resource_acl')
.count()
.where({ userId })
return parseInt(rawCount.count as string)
}
async findUsersResource ({
resourceId,
userId
}: ResourceAcl): Promise<ResourceAcl | null> {
return (
(await this.db<ResourceAcl>('resource_acl')
.where({ userId, resourceId })
.first()) ?? null
)
}
async queryResources ({
userId,
limit,
cursor
}: {
userId: string
limit: number
cursor: string | null
}): Promise<Resource[]> {
let query = this.db<Resource & ResourceAcl>('resources')
.join('resource_acl', 'resources.id', 'resource_acl.resourceId')
.where({ userId })
if (cursor) {
query = query.andWhere('createdAt', '<', cursor)
} }
const items = await query.orderBy('createdAt', 'desc').limit(limit)
return items
}
async countResourceComments (resourceId: string): Promise<number> { export const saveCommentFactory =
const [rawCount] = await this.db<Comment>('comments') ({ db }: { db: Knex }) =>
.count() async (comment: Comment): Promise<void> => {
.where({ resourceId }) await db<Comment>('comments').insert(comment)
return parseInt(rawCount.count as string)
}
async queryComments ({
resourceId,
limit,
cursor
}: {
resourceId: string
limit: number
cursor: string | null
}): Promise<Comment[]> {
let query = this.db<Comment>('comments').where({ resourceId })
if (cursor) {
query = query.andWhere('createdAt', '<', cursor)
} }
return await query.orderBy('createdAt', 'desc').limit(limit)
}
async queryRegions ( export const countCommentsFactory =
params: ({ db }: { db: Knex }) =>
| { async (resourceId: string): Promise<number> => {
connectionString?: string | undefined const [rawCount] = await db<Comment>('comments')
} .count()
| undefined = undefined
): Promise<Region[]> {
const query = this.db<Region>('regions')
if ((params != null) && params.connectionString) query.where(params)
return await query.select()
}
async findRegion (id: string): Promise<Region | null> {
return (await this.db<Region>('regions').where({ id }).first()) ?? null
}
async queryOrganizationsRegions (): Promise<OrganizationsRegions[]> {
return await this.db<OrganizationsRegions>('organizations_regions').select()
}
async findOrganizationRegion ({
regionId,
organizationId
}: OrganizationsRegions): Promise<OrganizationsRegions | null> {
return (
(await this.db<OrganizationsRegions>('organizations_regions')
.where({ regionId, organizationId })
.first()) ?? null
)
}
async saveRegion (region: Region): Promise<void> {
await this.db<Region>('regions').insert(region)
}
async saveOrganization (organization: Organization) {
await this.db<Organization>('organizations').insert(organization)
}
async findOrganization (id: string): Promise<Organization | null> {
return (
(await this.db<Organization>('organizations').where({ id }).first()) ??
null
)
}
async queryOrganizations (): Promise<Organization[]> {
return await this.db<Organization>('organizations').select()
}
async saveOrganizationRegion (or: OrganizationsRegions): Promise<void> {
return await this.db<OrganizationsRegions>('organizations_regions').insert(
or
)
}
async saveOrganizationAcl (orgAcl: OrganizationAcl): Promise<void> {
await this.db<OrganizationsRegions>('organization_acl').insert(orgAcl)
}
async findOrganizationAcl ({
userId,
organizationId
}: OrganizationAcl): Promise<OrganizationAcl | null> {
return (
(await this.db<OrganizationAcl>('organization_acl')
.where({ userId, organizationId })
.first()) ?? null
)
}
async saveOrganizationResourceAcl (
item: OrganizationResourceAcl
): Promise<void> {
await this.db<OrganizationResourceAcl>('organization_resource_acl').insert(
item
)
}
async findResourceRegion ({
resourceId
}: {
resourceId: string
}): Promise<ResourceRegion | null> {
return (
(await this.db<ResourceRegion>('resource_region')
.where({ resourceId }) .where({ resourceId })
.first()) ?? null return parseInt(rawCount.count as string)
) }
}
async saveResourceRegion (item: ResourceRegion): Promise<void> { export const findUserFactory =
await this.db<ResourceRegion>('resource_region').insert(item) ({ db }: { db: Knex }) =>
} async (userId: string): Promise<UserRecord | null> => {
} return (
(await db<UserRecord>('users').where('id', '=', userId).first()) ?? null
)
}
export const queryUsersFactoy =
({ db }: { db: Knex }) =>
async (): Promise<UserRecord[]> => {
return await db<UserRecord>('users').select()
}
export const saveUserFactory =
({ db }: { db: Knex }) =>
async (user: UserRecord): Promise<void> => {
await db<UserRecord>('users').insert(user)
}
export const getUsersResourceAclFactory =
({ db }: { db: Knex }) =>
async ({ resourceId, userId }: ResourceAcl): Promise<ResourceAcl | null> => {
return (
(await db<ResourceAcl>('resource_acl')
.where({ userId, resourceId })
.first()) ?? null
)
}
export const saveResourceAclFactory =
({ db }: { db: Knex }) =>
async (resourceAcl: ResourceAcl): Promise<void> => {
await db<ResourceAcl>('resource_acl').insert(resourceAcl)
}
export const countUsersResourcesFactory =
({ db }: { db: Knex }) =>
async (userId: string): Promise<number> => {
const [rawCount] = await db<ResourceAcl>('resource_acl')
.count()
.where({ userId })
return parseInt(rawCount.count as string)
}
export const findUsersResourceFactory =
({ db }: { db: Knex }) =>
async ({ resourceId, userId }: ResourceAcl): Promise<ResourceAcl | null> => {
return (
(await db<ResourceAcl>('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<Resource[]> => {
let query = db<Resource & ResourceAcl>('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
}
export const countResourceCommentsFactory =
({ db }: { db: Knex }) =>
async (resourceId: string): Promise<number> => {
const [rawCount] = await db<Comment>('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<Comment[]> => {
let query = db<Comment>('comments').where({ resourceId })
if (cursor !== null) {
query = query.andWhere('createdAt', '<', cursor)
}
return await query.orderBy('createdAt', 'desc').limit(limit)
}
export const queryRegionsFactory =
({ db }: { db: Knex }) =>
async (
params:
| {
connectionString?: string | undefined
}
| undefined = undefined
): Promise<Region[]> => {
let query = db<Region>('regions')
if (params?.connectionString !== undefined) query = query.where(params)
return await query.select()
}
export const findRegionFactory =
({ db }: { db: Knex }) =>
async (id: string): Promise<Region | null> => {
return (await db<Region>('regions').where({ id }).first()) ?? null
}
export const queryOrganizationsRegionsFactory =
({ db }: { db: Knex }) =>
async (): Promise<OrganizationsRegions[]> => {
return await db<OrganizationsRegions>('organizations_regions').select()
}
export const findOrganizationRegionFactory =
({ db }: { db: Knex }) =>
async ({
regionId,
organizationId
}: OrganizationsRegions): Promise<OrganizationsRegions | null> => {
return (
(await db<OrganizationsRegions>('organizations_regions')
.where({ regionId, organizationId })
.first()) ?? null
)
}
export const saveRegionFactory =
({ db }: { db: Knex }) =>
async (region: Region): Promise<void> => {
await db<Region>('regions').insert(region)
}
export const saveOrganizationFactory =
({ db }: { db: Knex }) =>
async (organization: Organization): Promise<void> => {
await db<Organization>('organizations').insert(organization)
}
export const findOrganizationFactory =
({ db }: { db: Knex }) =>
async (id: string): Promise<Organization | null> => {
return (
(await db<Organization>('organizations').where({ id }).first()) ?? null
)
}
export const queryOrganizationsFactory =
({ db }: { db: Knex }) =>
async (): Promise<Organization[]> => {
return await db<Organization>('organizations').select()
}
export const saveOrganizationRegionFactory =
({ db }: { db: Knex }) =>
async (or: OrganizationsRegions): Promise<void> => {
return await db<OrganizationsRegions>('organizations_regions').insert(or)
}
export const saveOrganizationAclFactory =
({ db }: { db: Knex }) =>
async (orgAcl: OrganizationAcl): Promise<void> => {
await db<OrganizationsRegions>('organization_acl').insert(orgAcl)
}
export const findOrganizationAclFactory =
({ db }: { db: Knex }) =>
async ({
userId,
organizationId
}: OrganizationAcl): Promise<OrganizationAcl | null> => {
return (
(await db<OrganizationAcl>('organization_acl')
.where({ userId, organizationId })
.first()) ?? null
)
}
export const saveOrganizationResourceAclFactory =
({ db }: { db: Knex }) =>
async (item: OrganizationResourceAcl): Promise<void> => {
await db<OrganizationResourceAcl>('organization_resource_acl').insert(item)
}
export const findResourceRegionFactory =
({ db }: { db: Knex }) =>
async ({
resourceId
}: {
resourceId: string
}): Promise<ResourceRegion | null> => {
return (
(await db<ResourceRegion>('resource_region')
.where({ resourceId })
.first()) ?? null
)
}
export const saveResourceRegionFactory =
({ db }: { db: Knex }) =>
async (item: ResourceRegion): Promise<void> => {
await db<ResourceRegion>('resource_region').insert(item)
}
+78 -46
View File
@@ -1,6 +1,9 @@
import { RegionRepo, MainRepo } from './repositories' import { getCommentsFactory } from './services/comments'
import { getComments } from './services/comments' import awilix from 'awilix'
import { createResource, getResources } from './services/resources' import {
createResourceFactory,
getResourcesFactory
} from './services/resources'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { import {
Resource, Resource,
@@ -16,29 +19,51 @@ import {
import { import {
createOrganization, createOrganization,
registerRegion, registerRegion,
getMainRepo, getResourceDb,
getRegionRepo, getMainDbClient,
getResourceRepo getRegionDb
} from './services/databaseManagement' } from './services/databaseManagement'
import { authorizeUserOrgRegion } from './services/authz' import { authorizeUserOrgRegionFactory } from './services/authz'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import {
countCommentsFactory,
countUsersResourcesFactory,
findOrganizationAclFactory,
findOrganizationRegionFactory,
findResourceFactory,
findUserFactory,
getUsersResourceAclFactory,
queryCommentsFactory,
queryOrganizationsFactory,
queryRegionsFactory,
queryResourcesFactory,
queryUsersFactoy,
saveCommentFactory,
saveOrganizationAclFactory,
saveOrganizationRegionFactory,
saveOrganizationResourceAclFactory,
saveResourceAclFactory,
saveResourceRegionFactory,
saveUserFactory
} from './repositories'
import { container } from './iocContainer'
const db = getMainDbClient()
// Resolvers define how to fetch the types defined in your schema. // Resolvers define how to fetch the types defined in your schema.
// This resolver retrieves books from the "books" array above. // This resolver retrieves books from the "books" array above.
export const resolvers = { export const resolvers = {
Query: { Query: {
async users () { async users () {
return await getMainRepo().queryUsers() return await queryUsersFactoy({ db })()
}, },
async user (_: unknown, args: { id: string }) { async user (_: unknown, args: { id: string }) {
return await getMainRepo().findUser(args.id) return await findUserFactory({ db })(args.id)
}, },
async resource ( async resource (
_: unknown, _: unknown,
args: { id: string, userId: string } args: { id: string, userId: string }
): Promise<Resource> { ): Promise<Resource> {
const mainRepo = getMainRepo() const maybeAcl = await getUsersResourceAclFactory({ db })({
const maybeAcl = await mainRepo.getUsersResourceAcl({
userId: args.userId, userId: args.userId,
resourceId: args.id resourceId: args.id
}) })
@@ -52,8 +77,10 @@ export const resolvers = {
} }
) )
} }
const resourceRepo = await getResourceRepo(args.id) const resourceDb = await getResourceDb(args.id)
const maybeResource = await resourceRepo.findResource(args.id) const maybeResource = await findResourceFactory({ db: resourceDb })(
args.id
)
if (maybeResource == null) { if (maybeResource == null) {
throw new GraphQLError('Resource not found', { throw new GraphQLError('Resource not found', {
extensions: { code: 'RESOURCE_NOT_FOUND' } extensions: { code: 'RESOURCE_NOT_FOUND' }
@@ -62,18 +89,17 @@ export const resolvers = {
return maybeResource return maybeResource
}, },
async organizations () { async organizations () {
return await getMainRepo().queryOrganizations() return await queryOrganizationsFactory({ db })()
}, },
async regions () { async regions () {
return await getMainRepo().queryRegions() return await queryRegionsFactory({ db })()
} }
}, },
User: { User: {
async resources (parent: UserRecord, args: PaginationArgs) { async resources (parent: UserRecord, args: PaginationArgs) {
const mainRepo = getMainRepo() return await getResourcesFactory(
return await getResources( countUsersResourcesFactory({ db }),
mainRepo.countUsersResources.bind(mainRepo), queryResourcesFactory({ db })
mainRepo.queryResources.bind(mainRepo)
)({ userId: parent.id, ...args }) )({ userId: parent.id, ...args })
} }
}, },
@@ -82,10 +108,10 @@ export const resolvers = {
parent: Resource, parent: Resource,
{ limit, cursor }: PaginationArgs { limit, cursor }: PaginationArgs
): Promise<CommentCollection> { ): Promise<CommentCollection> {
const resourceRepo = await getResourceRepo(parent.id) const resourceDb = await getResourceDb(parent.id)
return await getComments( return await getCommentsFactory(
resourceRepo.countComments.bind(resourceRepo), countCommentsFactory({ db: resourceDb }),
resourceRepo.queryComments.bind(resourceRepo) queryCommentsFactory({ db: resourceDb })
)({ )({
resourceId: parent.id, resourceId: parent.id,
limit, limit,
@@ -99,7 +125,7 @@ export const resolvers = {
{ input: { name } }: { input: UserCreateArgs } { input: { name } }: { input: UserCreateArgs }
) { ) {
const id = cryptoRandomString({ length: 10 }) const id = cryptoRandomString({ length: 10 })
await getMainRepo().saveUser({ id, name }) await saveUserFactory({ db })({ id, name })
return id return id
}, },
async registerRegion ( async registerRegion (
@@ -116,40 +142,45 @@ export const resolvers = {
return await createOrganization(args.name) return await createOrganization(args.name)
}, },
async addRegionToOrganization (_: unknown, args: OrganizationsRegions) { async addRegionToOrganization (_: unknown, args: OrganizationsRegions) {
await getMainRepo().saveOrganizationRegion(args) await saveOrganizationRegionFactory({ db })(args)
}, },
async addUserToOrganization ( async addUserToOrganization (
_: unknown, _: unknown,
{ input: args }: { input: OrganizationAcl } { input: args }: { input: OrganizationAcl }
) { ) {
await getMainRepo().saveOrganizationAcl(args) await saveOrganizationAclFactory({ db })(args)
}, },
async createResource ( async createResource (
_: unknown, _: unknown,
{ input: args }: { input: ResourceCreateArgs } { input: args }: { input: ResourceCreateArgs }
) { ) {
const mainRepo = getMainRepo() await authorizeUserOrgRegionFactory(
await authorizeUserOrgRegion( findOrganizationAclFactory({ db }),
mainRepo.findOrganizationAcl.bind(mainRepo), findOrganizationRegionFactory({ db })
mainRepo.findOrganizationRegion.bind(mainRepo)
)(args) )(args)
const repo = args.regionId const resourceDb =
? await getRegionRepo({ regionId: args.regionId }) args.regionId !== null
: mainRepo ? await getRegionDb({ regionId: args.regionId })
: db
const resourceId = await createResource( const requestContainer = container.createScope()
repo.saveResource.bind(repo), requestContainer.register({ resourceDb: awilix.asValue(resourceDb) })
mainRepo.saveResourceAcl.bind(mainRepo)
)(args)
if (args.organizationId) { const saveResource = requestContainer.resolve('saveResource')
await mainRepo.saveOrganizationResourceAcl({
const resourceId = await createResourceFactory({
saveResource,
saveResourceAcl: saveResourceAclFactory({ db })
})(args)
if (args.organizationId !== null) {
await saveOrganizationResourceAclFactory({ db })({
organizationId: args.organizationId, organizationId: args.organizationId,
resourceId resourceId
}) })
if (args.regionId) { if (args.regionId !== null) {
await mainRepo.saveResourceRegion({ await saveResourceRegionFactory({ db })({
resourceId, resourceId,
// i know its not null here, the authz function ensures it // i know its not null here, the authz function ensures it
regionId: args.regionId regionId: args.regionId
@@ -162,15 +193,16 @@ export const resolvers = {
_: unknown, _: unknown,
{ input: args }: { input: CommentCreateArgs } { input: args }: { input: CommentCreateArgs }
) { ) {
const mainRepo = getMainRepo() const resourceAcl = await getUsersResourceAclFactory({ db })(args)
const resourceAcl = await mainRepo.getUsersResourceAcl(args) if (resourceAcl == null) {
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 // 2. get resource db client
const resourceRepo = await getResourceRepo(args.resourceId) const resourceDb = await getResourceDb(args.resourceId)
// 3. save comment to db // 3. save comment to db
const id = cryptoRandomString({ length: 10 }) const id = cryptoRandomString({ length: 10 })
const createdAt = new Date() const createdAt = new Date()
await resourceRepo.saveComment({ id, createdAt, ...args }) await saveCommentFactory({ db: resourceDb })({ id, createdAt, ...args })
return id return id
} }
} }
+10 -4
View File
@@ -4,7 +4,7 @@ import {
UserOrgRegionArgs UserOrgRegionArgs
} from '../types' } from '../types'
export const authorizeUserOrgRegion = export const authorizeUserOrgRegionFactory =
( (
orgAclGetter: (params: OrganizationAcl) => Promise<OrganizationAcl | null>, orgAclGetter: (params: OrganizationAcl) => Promise<OrganizationAcl | null>,
orgRegionGetter: ( orgRegionGetter: (
@@ -12,12 +12,18 @@ export const authorizeUserOrgRegion =
) => Promise<OrganizationsRegions | null> ) => Promise<OrganizationsRegions | null>
) => ) =>
async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => { async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => {
if (!organizationId && regionId) { throw new Error("public org doesn't support regions") } if (!organizationId && regionId) {
throw new Error("public org doesn't support regions")
}
if (organizationId) { if (organizationId) {
if (!regionId) throw new Error('organizations can only write to regions') if (!regionId) throw new Error('organizations can only write to regions')
const orgAcl = await orgAclGetter({ organizationId, userId }) const orgAcl = await orgAclGetter({ organizationId, userId })
if (orgAcl == null) { throw new Error("user doesn't have access to this organization") } if (orgAcl == null) {
throw new Error("user doesn't have access to this organization")
}
const orgRegion = await orgRegionGetter({ organizationId, regionId }) const orgRegion = await orgRegionGetter({ organizationId, regionId })
if (orgRegion == null) { throw new Error('organization doesnt have access to this region') } if (orgRegion == null) {
throw new Error('organization doesnt have access to this region')
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ interface GetCommentsArgs extends PaginationArgs {
resourceId: string resourceId: string
} }
export const getComments = export const getCommentsFactory =
( (
countComments: (resourceId: string) => Promise<number>, countComments: (resourceId: string) => Promise<number>,
queryComments: (params: GetCommentsArgs) => Promise<Comment[]> queryComments: (params: GetCommentsArgs) => Promise<Comment[]>
+51 -41
View File
@@ -1,7 +1,13 @@
import { POSTGRES_URL } from '../config' import { POSTGRES_URL } from '../config'
import { RegionRepo, MainRepo } from '../repositories'
import knex, { Knex } from 'knex' import knex, { Knex } from 'knex'
import cryptoRandomString from 'crypto-random-string' import cryptoRandomString from 'crypto-random-string'
import {
findRegionFactory,
findResourceRegionFactory,
queryRegionsFactory,
saveOrganizationFactory,
saveRegionFactory
} from '../repositories'
const migrateToLatest = async (db: Knex): Promise<void> => { const migrateToLatest = async (db: Knex): Promise<void> => {
const plannedMigrations: Array<{ file: string }> = ( const plannedMigrations: Array<{ file: string }> = (
@@ -21,11 +27,11 @@ const migrateToLatest = async (db: Knex): Promise<void> => {
} }
export const migrateAll = async (): Promise<void> => { export const migrateAll = async (): Promise<void> => {
await migrateToLatest(mainRepo.db) await migrateToLatest(db)
const repos = await getAllRepositories() const dbClients = await getAllDbClients()
await Promise.all([ await Promise.all([
...repos.map(async (repo) => await migrateToLatest(repo.db)) ...dbClients.map(async (db) => await migrateToLatest(db))
]) ])
} }
@@ -52,29 +58,33 @@ const createDatabaseConfig = (
return config return config
} }
const mainRepo = new MainRepo(knex(createDatabaseConfig(POSTGRES_URL, null))) const db = knex(createDatabaseConfig(POSTGRES_URL, null))
const _repoStore: Map<string, RegionRepo> = new Map() const dbClientStore: Map<string, Knex> = new Map()
export const getRegionRepo = async ({
const findRegion = findRegionFactory({ db })
export const getRegionDb = async ({
regionId regionId
}: { }: {
regionId: string | undefined regionId: string | undefined
}): Promise<RegionRepo> => { }): Promise<Knex> => {
if (!regionId) return mainRepo if (!regionId) return db
const maybeRepo = _repoStore.get(regionId) const maybeClient = dbClientStore.get(regionId)
if (maybeRepo != null) return maybeRepo if (maybeClient != null) return maybeClient
const maybeRegion = await mainRepo.findRegion(regionId) const maybeRegion = await findRegion(regionId)
if (maybeRegion == null) throw Error(`region ${regionId} not found`) if (maybeRegion == null) throw Error(`region ${regionId} not found`)
const repo = new RegionRepo( const client = knex(
knex( createDatabaseConfig(maybeRegion.connectionString, maybeRegion.sslCaCert)
createDatabaseConfig(maybeRegion.connectionString, maybeRegion.sslCaCert)
)
) )
_repoStore.set(regionId, repo) dbClientStore.set(regionId, client)
return repo return client
} }
export const getMainRepo = (): MainRepo => mainRepo export const getMainDbClient = (): Knex => db
const queryRegions = queryRegionsFactory({ db })
const saveRegion = saveRegionFactory({ db })
export const registerRegion = async ({ export const registerRegion = async ({
name, name,
@@ -85,30 +95,28 @@ export const registerRegion = async ({
connectionString: string connectionString: string
sslCaCert: string | null sslCaCert: string | null
}): Promise<string> => { }): Promise<string> => {
const regions = await mainRepo.queryRegions({ connectionString }) const regions = await queryRegions({ connectionString })
if (regions.length > 0) throw new Error('This region is already registered') if (regions.length > 0) throw new Error('This region is already registered')
const id = cryptoRandomString({ length: 10 }) const id = cryptoRandomString({ length: 10 })
const repo = new RegionRepo( const newDb = knex(createDatabaseConfig(connectionString, sslCaCert))
knex(createDatabaseConfig(connectionString, sslCaCert)) await migrateToLatest(newDb)
) dbClientStore.set(id, newDb)
await migrateToLatest(repo.db)
_repoStore.set(id, repo)
const sslmode = sslCaCert ? 'require' : 'disable' const sslmode = sslCaCert ? 'require' : 'disable'
await setUpUserReplication({ await setUpUserReplication({
from: mainRepo.db, from: db,
to: repo.db, to: newDb,
regionName: name, regionName: name,
sslmode sslmode
}) })
await setUpResourceReplication({ await setUpResourceReplication({
from: repo.db, from: newDb,
to: mainRepo.db, to: db,
regionName: name, regionName: name,
sslmode sslmode
}) })
await mainRepo.saveRegion({ await saveRegion({
id, id,
name, name,
connectionString, connectionString,
@@ -117,9 +125,11 @@ export const registerRegion = async ({
return id return id
} }
const saveOrganization = saveOrganizationFactory({ db })
export const createOrganization = async (name: string): Promise<string> => { export const createOrganization = async (name: string): Promise<string> => {
const id = cryptoRandomString({ length: 10 }) const id = cryptoRandomString({ length: 10 })
await mainRepo.saveOrganization({ id, name }) await saveOrganization({ id, name })
return id return id
} }
@@ -196,17 +206,17 @@ const setUpResourceReplication = async ({
} }
} }
export const getAllRepositories = async (): Promise<RegionRepo[]> => { export const getAllDbClients = async (): Promise<Knex[]> => {
const regions = await mainRepo.queryRegions({}) const regions = await queryRegions({})
const regionRepos = await Promise.all( const regionClients = await Promise.all(
regions.map(async (region) => await getRegionRepo({ regionId: region.id })) regions.map(async (region) => await getRegionDb({ regionId: region.id }))
) )
return [mainRepo, ...regionRepos] return [db, ...regionClients]
} }
export const getResourceRepo = async ( const findResourceRegion = findResourceRegionFactory({ db })
resourceId: string
): Promise<RegionRepo> => { export const getResourceDb = async (resourceId: string): Promise<Knex> => {
const resourceRegion = await mainRepo.findResourceRegion({ resourceId }) const resourceRegion = await findResourceRegion({ resourceId })
return (resourceRegion != null) ? await getRegionRepo(resourceRegion) : getMainRepo() return resourceRegion != null ? await getRegionDb(resourceRegion) : db
} }
+11 -8
View File
@@ -10,7 +10,7 @@ import {
interface GetResourcesArgs extends PaginationArgs { interface GetResourcesArgs extends PaginationArgs {
userId: string userId: string
} }
export const getResources = export const getResourcesFactory =
( (
countResources: (userId: string) => Promise<number>, countResources: (userId: string) => Promise<number>,
queryResources: (params: GetResourcesArgs) => Promise<Resource[]> queryResources: (params: GetResourcesArgs) => Promise<Resource[]>
@@ -29,11 +29,14 @@ export const getResources =
} }
} }
export const createResource = export const createResourceFactory =
( ({
resourceSaver: (resource: Resource) => Promise<void>, saveResource,
resourceAclSaver: (resourceAcl: ResourceAcl) => Promise<void> saveResourceAcl
) => }: {
saveResource: (resource: Resource) => Promise<void>
saveResourceAcl: (resourceAcl: ResourceAcl) => Promise<void>
}) =>
async ({ userId, name }: ResourceCreateArgs): Promise<string> => { async ({ userId, name }: ResourceCreateArgs): Promise<string> => {
// 1. if no org, create project in main region, validate that, regionId is null // 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 // 2. if org, validate if user has access to the org
@@ -41,7 +44,7 @@ export const createResource =
// 4. create resource // 4. create resource
const id = cryptoRandomString({ length: 10 }) const id = cryptoRandomString({ length: 10 })
const resource = { id, name, createdAt: new Date() } const resource = { id, name, createdAt: new Date() }
await resourceSaver(resource) await saveResource(resource)
await resourceAclSaver({ resourceId: id, userId }) await saveResourceAcl({ resourceId: id, userId })
return id return id
} }
-29
View File
@@ -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()
})
})