feat(ioc): awilix POC

This commit is contained in:
Gergő Jedlicska
2024-09-17 15:42:51 +02:00
parent daea6d3765
commit 5b06afcfb1
9 changed files with 452 additions and 450 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"cSpell.words": ["awilix"]
}
+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')
+164 -164
View File
@@ -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<void> => {
await db<Resource>("resources").insert(resource);
};
async (resource: Resource): Promise<void> => {
await db<Resource>('resources').insert(resource)
}
export const findResourceFactory =
({ db }: { db: Knex }) =>
async (resourceId: string): Promise<Resource | null> => {
return (
(await db<Resource>("resources").where({ id: resourceId }).first()) ??
async (resourceId: string): Promise<Resource | null> => {
return (
(await db<Resource>('resources').where({ id: resourceId }).first()) ??
null
);
};
)
}
export const saveCommentFactory =
({ db }: { db: Knex }) =>
async (comment: Comment): Promise<void> => {
await db<Comment>("comments").insert(comment);
};
async (comment: Comment): Promise<void> => {
await db<Comment>('comments').insert(comment)
}
export const countCommentsFactory =
({ db }: { db: Knex }) =>
async (resourceId: string): Promise<number> => {
const [rawCount] = await db<Comment>("comments")
.count()
.where({ resourceId });
return parseInt(rawCount.count as string);
};
async (resourceId: string): Promise<number> => {
const [rawCount] = await db<Comment>('comments')
.count()
.where({ resourceId })
return parseInt(rawCount.count as string)
}
export const findUserFactory =
({ db }: { db: Knex }) =>
async (userId: string): Promise<UserRecord | null> => {
return (
(await db<UserRecord>("users").where("id", "=", userId).first()) ?? null
);
};
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();
};
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);
};
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
);
};
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);
};
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);
};
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
);
};
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);
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
}
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);
};
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);
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)
}
return await query.orderBy("createdAt", "desc").limit(limit);
};
export const queryRegionsFactory =
({ db }: { db: Knex }) =>
async (
params:
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();
};
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;
};
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();
};
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
);
};
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);
};
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);
};
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
);
};
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();
};
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);
};
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);
};
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
);
};
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);
};
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
);
};
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);
};
async (item: ResourceRegion): Promise<void> => {
await db<ResourceRegion>('resource_region').insert(item)
}
+99 -93
View File
@@ -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<Resource> {
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<CommentCollection> {
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
}
}
}
+16 -16
View File
@@ -1,29 +1,29 @@
import {
OrganizationAcl,
OrganizationsRegions,
UserOrgRegionArgs,
} from "../types";
UserOrgRegionArgs
} from '../types'
export const authorizeUserOrgRegionFactory =
(
orgAclGetter: (params: OrganizationAcl) => Promise<OrganizationAcl | null>,
orgRegionGetter: (
params: OrganizationsRegions,
) => Promise<OrganizationsRegions | null>,
) => Promise<OrganizationsRegions | null>
) =>
async ({ userId, regionId, organizationId }: UserOrgRegionArgs) => {
if (!organizationId && regionId) {
throw new Error("public org doesn't support regions");
}
if (organizationId) {
if (!regionId) throw new Error("organizations can only write to regions");
const orgAcl = await orgAclGetter({ organizationId, userId });
if (orgAcl == null) {
throw new Error("user doesn't have access to this organization");
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')
}
}
}
};
+15 -15
View File
@@ -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<number>,
queryComments: (params: GetCommentsArgs) => Promise<Comment[]>,
queryComments: (params: GetCommentsArgs) => Promise<Comment[]>
) =>
async (params: GetCommentsArgs): Promise<CommentCollection> => {
async (params: GetCommentsArgs): Promise<CommentCollection> => {
// yes, i should be doing base64 de and encoding with the cursor...
const totalCount = await countComments(params.resourceId);
const items = await queryComments(params);
let cursor = null;
if (items.length > 0) {
cursor = items.slice(-1)[0].createdAt.toISOString();
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,
};
};
+105 -105
View File
@@ -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<void> => {
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<void> => {
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<string, Knex> = new Map();
const dbClientStore: Map<string, Knex> = new Map()
const findRegion = findRegionFactory({ db });
const findRegion = findRegionFactory({ db })
export const getRegionDb = async ({
regionId,
regionId
}: {
regionId: string | undefined;
regionId: string | undefined
}): Promise<Knex> => {
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<string> => {
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<string> => {
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<void> => {
// 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<void> => {
// 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<Knex[]> => {
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<Knex> => {
const resourceRegion = await findResourceRegion({ resourceId });
return resourceRegion != null ? await getRegionDb(resourceRegion) : db;
};
const resourceRegion = await findResourceRegion({ resourceId })
return resourceRegion != null ? await getRegionDb(resourceRegion) : db
}
+31 -28
View File
@@ -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<number>,
queryResources: (params: GetResourcesArgs) => Promise<Resource[]>,
queryResources: (params: GetResourcesArgs) => Promise<Resource[]>
) =>
async (params: GetResourcesArgs): Promise<ResourceCollection> => {
const totalCount = await countResources(params.userId);
const items = await queryResources(params);
let cursor = null;
if (items.length > 0) {
cursor = items.slice(-1)[0].createdAt.toISOString();
async (params: GetResourcesArgs): Promise<ResourceCollection> => {
const totalCount = await countResources(params.userId)
const items = await queryResources(params)
let cursor = null
if (items.length > 0) {
cursor = items.slice(-1)[0].createdAt.toISOString()
}
return {
totalCount,
items,
cursor
}
}
return {
totalCount,
items,
cursor,
};
};
export const createResourceFactory =
(
resourceSaver: (resource: Resource) => Promise<void>,
resourceAclSaver: (resourceAcl: ResourceAcl) => Promise<void>,
) =>
async ({ userId, name }: ResourceCreateArgs): Promise<string> => {
({
saveResource,
saveResourceAcl
}: {
saveResource: (resource: Resource) => Promise<void>
saveResourceAcl: (resourceAcl: ResourceAcl) => Promise<void>
}) =>
async ({ userId, name }: ResourceCreateArgs): Promise<string> => {
// 1. if no org, create project in main region, validate that, regionId is null
// 2. if org, validate if user has access to the org
// 3. if org and region, validate if org has access to region
// 4. create resource
const id = cryptoRandomString({ length: 10 });
const resource = { id, name, createdAt: new Date() };
await resourceSaver(resource);
await resourceAclSaver({ resourceId: id, userId });
return id;
};
const id = cryptoRandomString({ length: 10 })
const resource = { id, name, createdAt: new Date() }
await saveResource(resource)
await saveResourceAcl({ resourceId: id, userId })
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()
})
})