Compare commits
1 Commits
multi_org
...
awilix-poc
| Author | SHA1 | Date | |
|---|---|---|---|
| 3737ee06cd |
+30
-3
@@ -1,13 +1,40 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
maindb:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- 5454:5432
|
||||
volumes:
|
||||
- ./.postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_main
|
||||
|
||||
eu_db:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- 5455:5433
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_eu
|
||||
- PGPORT=5433
|
||||
|
||||
us_db:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- 5456:5434
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=speckle
|
||||
- POSTGRES_USER=speckle
|
||||
- POSTGRES_DB=speckle_us
|
||||
- PGPORT=5434
|
||||
|
||||
start_dependencies:
|
||||
image: ducktors/docker-wait-for-dependencies
|
||||
depends_on:
|
||||
- maindb
|
||||
- eu_db
|
||||
- us_db
|
||||
container_name: wait-for-dependencies
|
||||
command: maindb:5432 eu_db:5433 us_db:5434
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@
|
||||
"dev:old": "nodemon --ext ts,graphql --exec node --inspect -r @swc/register src/bin/www.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/app.js",
|
||||
"dev": "nodemon src/app.ts"
|
||||
"dev": "docker compose up start_dependencies && tsx src/app.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.0",
|
||||
"awilix": "^11.0.0",
|
||||
"dotenv": "^16.4.1",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-scalars": "^1.22.4",
|
||||
|
||||
Generated
+2788
-2210
File diff suppressed because it is too large
Load Diff
+41
-23
@@ -1,42 +1,60 @@
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { resolvers } from './resolvers'
|
||||
import { startStandaloneServer } from '@apollo/server/standalone'
|
||||
import { readFileSync } from 'fs'
|
||||
import { typeDefs as scalarTypeDefs } from 'graphql-scalars'
|
||||
import { knex } from './db'
|
||||
import { ApolloServer } from "@apollo/server";
|
||||
import { resolvers } from "./resolvers";
|
||||
import { startStandaloneServer } from "@apollo/server/standalone";
|
||||
import { readFileSync } from "fs";
|
||||
import { typeDefs as scalarTypeDefs } from "graphql-scalars";
|
||||
import { mainDb } from "./db";
|
||||
import Knex, { Knex as KnexType } from 'knex'
|
||||
import knexfile from "../knexfile";
|
||||
import { container } from "./compositionRoot";
|
||||
import { asValue } from "awilix";
|
||||
|
||||
const typeDefs = readFileSync('src/schema.graphql', { encoding: 'utf-8' })
|
||||
const typeDefs = readFileSync("src/schema.graphql", { encoding: "utf-8" });
|
||||
|
||||
// The ApolloServer constructor requires two parameters: your schema
|
||||
// definition and your set of resolvers.
|
||||
const server = new ApolloServer({
|
||||
typeDefs: [typeDefs, ...scalarTypeDefs],
|
||||
resolvers
|
||||
})
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const startServer = async (): Promise<void> => {
|
||||
const { url } = await startStandaloneServer(server, {
|
||||
listen: { port: 4000 }
|
||||
})
|
||||
|
||||
const plannedMigrations: Array<{ file: string }> = (
|
||||
await knex.migrate.list()
|
||||
)[1]
|
||||
await mainDb.migrate.list()
|
||||
)[1];
|
||||
if (plannedMigrations.length > 0) {
|
||||
console.log(
|
||||
`🕰️ planning migrations: ${plannedMigrations
|
||||
.map((m) => m.file)
|
||||
.join(',')}`
|
||||
)
|
||||
.join(",")}`,
|
||||
);
|
||||
}
|
||||
await mainDb.migrate.latest();
|
||||
|
||||
await knex.migrate.latest()
|
||||
const regions = await mainDb('regions')
|
||||
const regionalDBs = new Map<string, KnexType>(regions.map(region => [region.name, Knex({ ...knexfile, connection: region.connectionString })]))
|
||||
await Promise.all(Array.from(regionalDBs.values()).map(db => db.migrate.latest())).catch(err => console.log({ err }))
|
||||
|
||||
console.log(`🚀 Server ready at: ${url}`)
|
||||
}
|
||||
container.register({
|
||||
regionalDBs: asValue(regionalDBs),
|
||||
})
|
||||
|
||||
const { url } = await startStandaloneServer(server, {
|
||||
listen: { port: 3000 },
|
||||
context: async ({ req }) => {
|
||||
const scope = container.createScope(); // Create a scope per request
|
||||
return {
|
||||
container: scope,
|
||||
// Add any other custom context, like user from req if needed
|
||||
};
|
||||
},
|
||||
});
|
||||
console.log(`🚀 Server ready at: ${url}`);
|
||||
};
|
||||
|
||||
startServer()
|
||||
.then()
|
||||
.catch((err: Error) =>
|
||||
console.log(`🔥 failed to start server ${err.message}`)
|
||||
)
|
||||
.catch((err: Error) => {
|
||||
console.log({ err });
|
||||
console.log(`🔥 failed to start server ${err.message}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { asFunction, asValue, createContainer, InjectionMode } from 'awilix'
|
||||
import { mainDb } from './db'
|
||||
import { countComments, countResources, queryComments, queryResource, queryResourceAcl, queryUser } from './repositories'
|
||||
|
||||
export const container = createContainer({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
strict: true,
|
||||
}).register({
|
||||
mainDB: asValue(mainDb),
|
||||
queryUser: asFunction(queryUser).scoped(),
|
||||
queryResource: asFunction(queryResource).scoped(),
|
||||
queryResourceAcl: asFunction(queryResourceAcl).scoped(),
|
||||
countResources: asFunction(countResources).scoped(),
|
||||
countComments: asFunction(countComments).scoped(),
|
||||
queryComments: asFunction(queryComments).scoped(),
|
||||
})
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@ import 'dotenv/config'
|
||||
import { parseEnv } from 'znv'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const { POSTGRES_URL } = parseEnv(process.env, {
|
||||
POSTGRES_URL: z.string().min(1)
|
||||
const config = parseEnv(process.env, {
|
||||
MAIN_DB_URI: z.string().min(1)
|
||||
})
|
||||
|
||||
console.log([POSTGRES_URL].join(', '))
|
||||
export default config
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import Knex from 'knex'
|
||||
import config from '../knexfile'
|
||||
import config from './config'
|
||||
import knexfile from '../knexfile'
|
||||
|
||||
export const knex = Knex(config)
|
||||
const knexConfig = {
|
||||
...knexfile,
|
||||
connection: config.MAIN_DB_URI,
|
||||
}
|
||||
|
||||
export const mainDb = Knex(knexConfig)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable('regions', (table) => {
|
||||
table.text('id').primary()
|
||||
table.text('name')
|
||||
table.text('connectionString')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable('regions')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return await knex.schema.createTable('resource_meta', (table) => {
|
||||
table.text('id').primary()
|
||||
table.text('resourceId').references('id').inTable('resources')
|
||||
table.text('region')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.dropTable('resource_meta')
|
||||
}
|
||||
|
||||
+26
-19
@@ -1,22 +1,27 @@
|
||||
import { knex } from "./db";
|
||||
import { UserRecord, Resource, ResourceAcl, Comment } from "./types";
|
||||
import { Knex } from "knex";
|
||||
import { UserRecord, Resource, ResourceAcl, Comment, ResourceMeta } from "./types";
|
||||
|
||||
const Users = () => knex<UserRecord>("users");
|
||||
const Resources = () => knex<Resource>("resources");
|
||||
const ResourceAclRepo = () => knex<ResourceAcl>("resource_acl");
|
||||
const Comments = () => knex<Comment>("comments");
|
||||
const tables = {
|
||||
users: (db: Knex) => db<UserRecord>('users'),
|
||||
resources: (db: Knex) => db<Resource>('resources'),
|
||||
resourceAcl: (db: Knex) => db<ResourceAcl>('resource_acl'),
|
||||
comments: (db: Knex) => db<Comment>('comments'),
|
||||
resourceMeta: (db: Knex) => db<ResourceMeta>('resource_meta'),
|
||||
}
|
||||
|
||||
export const queryUser = async (userId: string): Promise<UserRecord | null> => {
|
||||
return (await Users().where("id", "=", userId).first()) ?? null;
|
||||
export const queryUser = ({ mainDB }: { mainDB: Knex }) => async (userId: string): Promise<UserRecord | null> => {
|
||||
return (await tables.users(mainDB).where("id", "=", userId).first()) ?? null;
|
||||
};
|
||||
|
||||
export const queryResource = async (
|
||||
export const queryResource = ({ mainDB, regionalDBs }: { mainDB: Knex, regionalDBs: Map<string, Knex> }) => async (
|
||||
resourceId: string,
|
||||
): Promise<Resource | null> => {
|
||||
return (await Resources().where("id", "=", resourceId).first()) ?? null;
|
||||
const resourceMeta = await tables.resourceMeta(mainDB).where({ resourceId }).first()
|
||||
const regionalDB = regionalDBs.get(resourceMeta!.region)!
|
||||
return (await tables.resources(regionalDB).where("id", "=", resourceId).first()) ?? null;
|
||||
};
|
||||
|
||||
export const queryResourceAcl = async ({
|
||||
export const queryResourceAcl = ({ mainDB }: { mainDB: Knex }) => async ({
|
||||
resourceId,
|
||||
userId,
|
||||
}: {
|
||||
@@ -24,19 +29,19 @@ export const queryResourceAcl = async ({
|
||||
userId: string;
|
||||
}): Promise<ResourceAcl | null> => {
|
||||
return (
|
||||
(await ResourceAclRepo()
|
||||
(await tables.resourceAcl(mainDB)
|
||||
.where("userId", "=", userId)
|
||||
.andWhere("resourceId", "=", resourceId)
|
||||
.first()) ?? null
|
||||
);
|
||||
};
|
||||
|
||||
export const countResources = async (userId: string): Promise<number> => {
|
||||
const [rawCount] = await ResourceAclRepo().count().where({ userId });
|
||||
export const countResources = ({ mainDB }: { mainDB: Knex }) => async (userId: string): Promise<number> => {
|
||||
const [rawCount] = await tables.resourceAcl(mainDB).count().where({ userId });
|
||||
return parseInt(rawCount.count as string);
|
||||
};
|
||||
|
||||
export const queryResources = async ({
|
||||
export const queryResources = ({ mainDB, regionalDBs }: { mainDB: Knex; regionalDBs: Record<string, Knex> }) => async ({
|
||||
userId,
|
||||
limit,
|
||||
cursor,
|
||||
@@ -45,6 +50,8 @@ export const queryResources = async ({
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
}) => {
|
||||
const resourceMeta = await tables.resourceMeta(mainDB).where({ resourceId }).first()
|
||||
const regionalDB = regionalDBs[resourceMeta!.region]
|
||||
const query = Resources()
|
||||
.join("resource_acl", "resources.id", "resource_acl.resourceId")
|
||||
.where({ userId });
|
||||
@@ -54,12 +61,12 @@ export const queryResources = async ({
|
||||
return await query.limit(limit);
|
||||
};
|
||||
|
||||
export const countComments = async (resourceId: string): Promise<number> => {
|
||||
const [rawCount] = await Comments().count().where({ resourceId });
|
||||
export const countComments = ({ mainDB }: { mainDB: Knex }) => async (resourceId: string): Promise<number> => {
|
||||
const [rawCount] = await tables.comments(mainDB).count().where({ resourceId });
|
||||
return parseInt(rawCount.count as string);
|
||||
};
|
||||
|
||||
export const queryComments = async ({
|
||||
export const queryComments = ({ mainDB }: { mainDB: Knex }) => async ({
|
||||
resourceId,
|
||||
limit,
|
||||
cursor,
|
||||
@@ -68,7 +75,7 @@ export const queryComments = async ({
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
}): Promise<Comment[]> => {
|
||||
const query = Comments().where({ resourceId });
|
||||
const query = tables.comments(mainDB).where({ resourceId });
|
||||
if (cursor) {
|
||||
query.andWhere("createdAt", "<", cursor);
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,4 +1,4 @@
|
||||
import { queryResourceAcl } from "./repositories";
|
||||
import { queryResource, queryResourceAcl } from "./repositories";
|
||||
import { getUser, getResource, getComments, getResources } from "./services";
|
||||
import { GraphQLError } from "graphql";
|
||||
import {
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
// This resolver retrieves books from the "books" array above.
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
async user(_: unknown, args: { id: string }) {
|
||||
return await getUser(args.id);
|
||||
async user(_: unknown, args: { id: string }, ctx) {
|
||||
return await ctx.container.cradle.queryUser(args.id);
|
||||
},
|
||||
async resource(
|
||||
_: unknown,
|
||||
args: { id: string; userId: string },
|
||||
ctx
|
||||
): Promise<Resource> {
|
||||
const maybeAcl = await queryResourceAcl({
|
||||
const maybeAcl = await ctx.container.cradle.queryResourceAcl({
|
||||
userId: args.userId,
|
||||
resourceId: args.id,
|
||||
});
|
||||
@@ -34,7 +35,7 @@ export const resolvers = {
|
||||
},
|
||||
);
|
||||
}
|
||||
const maybeResource = await getResource(args.id);
|
||||
const maybeResource = await ctx.container.cradle.queryResource(args.id);
|
||||
if (maybeResource == null) {
|
||||
throw new GraphQLError("Resource not found", {
|
||||
extensions: { code: "RESOURCE_NOT_FOUND" },
|
||||
|
||||
+9
-2
@@ -17,15 +17,16 @@ interface Collection<T> {
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export interface CommentCollection extends Collection<Comment> {}
|
||||
export interface CommentCollection extends Collection<Comment> { }
|
||||
|
||||
export interface Resource {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
region: string
|
||||
}
|
||||
|
||||
export interface ResourceCollection extends Collection<Resource> {}
|
||||
export interface ResourceCollection extends Collection<Resource> { }
|
||||
|
||||
export interface UserRecord {
|
||||
id: string;
|
||||
@@ -44,3 +45,9 @@ export interface ResourceAcl {
|
||||
userId: string;
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
export interface ResourceMeta {
|
||||
id: string
|
||||
region: string;
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user