Files
suaveplan/packages/api-stack/api/api-plugin-tenant/src/store/memory.ts
2026-05-27 23:29:53 +01:00

152 lines
5.1 KiB
TypeScript

import type { Tenant } from "@suaveplan/api-tenant";
import {
TENANT_PLUGIN_CODES,
TenantPluginError,
} from "../error-codes/error-codes.js";
import type {
CreateTenantInput,
ListTenantsQuery,
TenantStore,
UpdateTenantInput,
} from "./store.js";
/** Test- and small-ops-friendly in-memory `TenantStore` implementation. */
export class MemoryTenantStore implements TenantStore {
private readonly tenants = new Map<string, Tenant>();
private nextSeq = 1;
public async getById(id: string): Promise<Tenant | null> {
return this.tenants.get(id) ?? null;
}
public async getBySlug(slug: string): Promise<Tenant | null> {
for (const t of this.tenants.values()) {
if (t.slug === slug) return t;
}
return null;
}
public async getByDomain(domain: string): Promise<Tenant | null> {
for (const t of this.tenants.values()) {
if (t.domains.includes(domain)) return t;
}
return null;
}
public async listAll(query?: ListTenantsQuery): Promise<readonly Tenant[]> {
let rows = Array.from(this.tenants.values());
if (query?.status !== undefined) {
rows = rows.filter((t) => t.status === query.status);
}
const offset = query?.offset ?? 0;
const limit = query?.limit ?? rows.length;
return rows.slice(offset, offset + limit);
}
public async create(input: CreateTenantInput): Promise<Tenant> {
for (const t of this.tenants.values()) {
if (t.slug === input.slug) {
throw new TenantPluginError(
TENANT_PLUGIN_CODES.TENANT_SLUG_TAKEN,
`Tenant slug '${input.slug}' is already taken`,
{ context: { slug: input.slug, statusCode: 409 } }
);
}
}
const id = `tenant_${this.nextSeq++}`;
const now = new Date();
const tenant: Tenant = {
id,
name: input.name,
slug: input.slug,
status: input.status ?? "active",
settings: input.settings ?? {},
enabledPlugins: input.enabledPlugins
? [...input.enabledPlugins]
: [],
domains: input.domains ? [...input.domains] : [],
createdAt: now,
updatedAt: now,
...(input.plan !== undefined ? { plan: input.plan } : {}),
...(input.metadata !== undefined
? { metadata: input.metadata }
: {}),
};
this.tenants.set(id, tenant);
return tenant;
}
public async update(id: string, patch: UpdateTenantInput): Promise<Tenant> {
const existing = this.tenants.get(id);
if (!existing) {
throw new TenantPluginError(
TENANT_PLUGIN_CODES.TENANT_NOT_FOUND,
`Tenant '${id}' not found`,
{ context: { id, statusCode: 404 } }
);
}
if (patch.slug !== undefined && patch.slug !== existing.slug) {
for (const t of this.tenants.values()) {
if (t.id !== id && t.slug === patch.slug) {
throw new TenantPluginError(
TENANT_PLUGIN_CODES.TENANT_SLUG_TAKEN,
`Tenant slug '${patch.slug}' is already taken`,
{
context: {
slug: patch.slug,
statusCode: 409,
},
}
);
}
}
}
const updated: Tenant = {
...existing,
...(patch.name !== undefined ? { name: patch.name } : {}),
...(patch.slug !== undefined ? { slug: patch.slug } : {}),
...(patch.status !== undefined ? { status: patch.status } : {}),
...(patch.plan !== undefined ? { plan: patch.plan } : {}),
...(patch.enabledPlugins !== undefined
? { enabledPlugins: [...patch.enabledPlugins] }
: {}),
...(patch.domains !== undefined
? { domains: [...patch.domains] }
: {}),
...(patch.settings !== undefined
? { settings: patch.settings }
: {}),
...(patch.metadata !== undefined
? { metadata: patch.metadata }
: {}),
updatedAt: new Date(),
};
this.tenants.set(id, updated);
return updated;
}
public async delete(id: string): Promise<void> {
const existing = this.tenants.get(id);
if (!existing) {
// Idempotent — deleting an unknown id is a no-op.
return;
}
this.tenants.set(id, {
...existing,
status: "deleted",
updatedAt: new Date(),
});
}
/** Test helper — number of stored entries. */
public get size(): number {
return this.tenants.size;
}
/** Test helper — drop everything. */
public clear(): void {
this.tenants.clear();
this.nextSeq = 1;
}
}