Skip to content

Commit

Permalink
feat(multi-tenant): add tenant controller and resolver (#574)
Browse files Browse the repository at this point in the history
  • Loading branch information
dipendraupreti committed Jan 4, 2024
1 parent 258b09e commit 95bb1f9
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/multi-tenant/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ export { default } from "./plugin";

export { default as TenantService } from "./model/tenants/service";

export * as validateTenantSchema from "./lib/validateTenantSchema";

export { default as tenantMigrationPlugin } from "./migratePlugin";

export { default as thirdPartyEmailPassword } from "./supertokens/recipes";

export { default as tenantResolver } from "./model/tenants/resolver";

export { default as tenantRoutes } from "./model/tenants/controller";

export type {
MultiTenantConfig,
Tenant,
Expand Down
53 changes: 53 additions & 0 deletions packages/multi-tenant/src/lib/__test__/domainSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";

import { domainSchema } from "../validateTenantSchema";

describe.concurrent("domainSchema", () => {
it.each([
[undefined, true],
["example.com", true],
["www.ex-ample.com", true],
["ww-w.ex-ample.com", true],
["1-ww-w.ex-ample.com", true],
["example1.com", true],
["example-1.com", true],
["a-example.com", true],
["example.co", true],
["example.com.uk", true],
["example.edu", true],
["example.edu.au", true],
["example.comm", true],
["a.b.c.com", true],
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
true,
],
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com",
true,
],
["", false],
["1", false],
["a", false],
["A", false],
["a1", false],
["example", false],
[".com", false],
["1test", false],
["-ab", false],
["a1-", false],
["Example.com", false],
["example.COM", false],
["EXAMPLE.COM", false],
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
false,
],
[
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com",
false,
],
])("domainSchema.safeParse(slug) -> expected", async (slug, expected) => {
expect(domainSchema.safeParse(slug).success).toBe(expected);
});
});
30 changes: 30 additions & 0 deletions packages/multi-tenant/src/lib/__test__/slugSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";

import { slugSchema } from "../validateTenantSchema";

describe.concurrent("slugSchema", () => {
it.each([
["a", true],
["a1", true],
["abc", true],
["tenant1", true],
["a-b", true],
["a-1", true],
["", false],
["a-1", true],
["", false],
["1", false],
["12", false],
["1 2", false],
["a ", false],
["a b", false],
["A", false],
["Z1", false],
["1tenant", false],
["-ab", false],
["a1-", false],
[undefined, false],
])("slugSchema.safeParse(slug) -> expected", async (slug, expected) => {
expect(slugSchema.safeParse(slug).success).toBe(expected);
});
});
53 changes: 53 additions & 0 deletions packages/multi-tenant/src/lib/validateTenantSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { z } from "zod";

import getMultiTenantConfig from "./getMultiTenantConfig";

import type { ApiConfig } from "@dzangolab/fastify-config";
import type { PrimitiveValueExpression } from "slonik";

const domainSchema = z.optional(
z
.string()
.max(255)
.regex(/^([\da-z]([\da-z-]{0,61}[\da-z])?\.)+[a-z]{2,}$/)
);

const slugSchema = z.string().regex(/^(?!.*-+$)[a-z][\da-z-]{0,61}([\da-z])?$/);

const validateTenantInput = (
config: ApiConfig,
tenantInput: Record<string, PrimitiveValueExpression>
) => {
const tenantTableColumnConfig = getMultiTenantConfig(config).table.columns;

const mappedTenantInput = {
slug: tenantInput[tenantTableColumnConfig.slug],
domain: tenantInput[tenantTableColumnConfig.domain],
};

const tenantInputSchema = z.object({
slug: slugSchema,
domain: domainSchema,
});

tenantInputSchema.parse(mappedTenantInput);
};

const validateTenantUpdate = (
config: ApiConfig,
tenantUpdate: Record<string, PrimitiveValueExpression>
) => {
const tenantTableColumnConfig = getMultiTenantConfig(config).table.columns;

const mappedTenantUpdate = {
domain: tenantUpdate[tenantTableColumnConfig.domain],
};

const tenantInputSchema = z.object({
domain: domainSchema,
});

tenantInputSchema.parse(mappedTenantUpdate);
};

export { domainSchema, slugSchema, validateTenantInput, validateTenantUpdate };
37 changes: 37 additions & 0 deletions packages/multi-tenant/src/model/tenants/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import handlers from "./handlers";

import type { FastifyInstance } from "fastify";

const plugin = async (
fastify: FastifyInstance,
options: unknown,
done: () => void
) => {
fastify.get(
"/tenants",
{
preHandler: fastify.verifySession(),
},
handlers.tenants
);

fastify.get(
"/tenants/:id(^\\d+)",
{
preHandler: fastify.verifySession(),
},
handlers.tenant
);

fastify.post(
"/tenants",
{
preHandler: fastify.verifySession(),
},
handlers.create
);

done();
};

export default plugin;
20 changes: 20 additions & 0 deletions packages/multi-tenant/src/model/tenants/handlers/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { validateTenantInput } from "../../../lib/validateTenantSchema";
import Service from "../service";

import type { TenantCreateInput } from "../../../types";
import type { FastifyReply } from "fastify";
import type { SessionRequest } from "supertokens-node/framework/fastify";

const create = async (request: SessionRequest, reply: FastifyReply) => {
const input = request.body as TenantCreateInput;

validateTenantInput(request.config, input);

const service = new Service(request.config, request.slonik);

const data = await service.create(input);

reply.send(data);
};

export default create;
5 changes: 5 additions & 0 deletions packages/multi-tenant/src/model/tenants/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import create from "./create";
import tenant from "./tenant";
import tenants from "./tenants";

export default { create, tenant, tenants };
16 changes: 16 additions & 0 deletions packages/multi-tenant/src/model/tenants/handlers/tenant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Service from "../service";

import type { FastifyReply } from "fastify";
import type { SessionRequest } from "supertokens-node/framework/fastify";

const tenant = async (request: SessionRequest, reply: FastifyReply) => {
const service = new Service(request.config, request.slonik, request.dbSchema);

const { id } = request.params as { id: number };

const data = await service.findById(id);

reply.send(data);
};

export default tenant;
26 changes: 26 additions & 0 deletions packages/multi-tenant/src/model/tenants/handlers/tenants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Service from "../service";

import type { FastifyReply } from "fastify";
import type { SessionRequest } from "supertokens-node/framework/fastify";

const tenants = async (request: SessionRequest, reply: FastifyReply) => {
const service = new Service(request.config, request.slonik, request.dbSchema);

const { limit, offset, filters, sort } = request.query as {
limit: number;
offset?: number;
filters?: string;
sort?: string;
};

const data = await service.list(
limit,
offset,
filters ? JSON.parse(filters) : undefined,
sort ? JSON.parse(sort) : undefined
);

reply.send(data);
};

export default tenants;
74 changes: 74 additions & 0 deletions packages/multi-tenant/src/model/tenants/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Service from "./service";
import { validateTenantInput } from "../../lib/validateTenantSchema";

import type { TenantCreateInput } from "./../../types";
import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik";
import type { MercuriusContext } from "mercurius";

const Mutation = {
createTenant: async (
parent: unknown,
arguments_: {
data: {
id: string;
password: string;
};
},
context: MercuriusContext
) => {
const input = arguments_.data as TenantCreateInput;

validateTenantInput(context.config, input);

const service = new Service(
context.config,
context.database,
context.dbSchema
);

return await service.create(input);
},
};

const Query = {
tenant: async (
parent: unknown,
arguments_: { id: number },
context: MercuriusContext
) => {
const service = new Service(
context.config,
context.database,
context.dbSchema
);

return await service.findById(arguments_.id);
},
tenants: async (
parent: unknown,
arguments_: {
limit: number;
offset: number;
filters?: FilterInput;
sort?: SortInput[];
},
context: MercuriusContext
) => {
const service = new Service(
context.config,
context.database,
context.dbSchema
);

return await service.list(
arguments_.limit,
arguments_.offset,
arguments_.filters
? JSON.parse(JSON.stringify(arguments_.filters))
: undefined,
arguments_.sort ? JSON.parse(JSON.stringify(arguments_.sort)) : undefined
);
},
};

export default { Mutation, Query };
16 changes: 16 additions & 0 deletions packages/multi-tenant/src/model/tenants/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ class TenantService<
return tenants as Tenant[];
};

create = async (data: TenantCreateInput): Promise<Tenant | undefined> => {
const query = this.factory.getCreateSql(data);

const result = (await this.database.connect(async (connection) => {
return connection.query(query).then((data) => {
return data.rows[0];
});
})) as Tenant;

return result ? this.postCreate(result) : undefined;
};

findByHostname = async (hostname: string): Promise<Tenant | null> => {
const query = this.factory.getFindByHostnameSql(
hostname,
Expand Down Expand Up @@ -61,6 +73,10 @@ class TenantService<
>;
}

get sortKey(): string {
return this.config.multiTenant.table?.columns?.id || super.sortKey;
}

get table() {
return this.config.multiTenant?.table?.name || "tenants";
}
Expand Down
8 changes: 8 additions & 0 deletions packages/multi-tenant/src/model/tenants/sqlFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ class TenantSqlFactory<
return query;
};

getFindByIdSql = (id: number | string): QuerySqlToken => {
return sql.type(this.validationSchema)`
SELECT *
FROM ${this.getTableFragment()}
WHERE ${sql.identifier([this.getMappedField("id")])} = ${id};
`;
};

protected getAliasedField = (field: string) => {
const mapped = this.getMappedField(field);

Expand Down

0 comments on commit 95bb1f9

Please sign in to comment.