@manablox/db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import type { ContentTypeRegistry } from '@manablox/core';
2
+ import type { Database } from '../client.js';
3
+ import { AssetRepository } from './asset.js';
4
+ import { AssetUsageRepository } from './asset-usage.js';
5
+ import { ContentRepository } from './content.js';
6
+ import { ContentTypeRepository } from './content-type.js';
7
+ import { SpaceRepository } from './space.js';
8
+ import { UserRepository } from './user.js';
9
+
10
+ export interface Repositories {
11
+ content: ContentRepository;
12
+ contentTypes: ContentTypeRepository;
13
+ spaces: SpaceRepository;
14
+ assets: AssetRepository;
15
+ assetUsages: AssetUsageRepository;
16
+ users: UserRepository;
17
+ }
18
+
19
+ export function createRepositories(db: Database, registry: ContentTypeRegistry): Repositories {
20
+ return {
21
+ content: new ContentRepository(db, registry),
22
+ contentTypes: new ContentTypeRepository(db),
23
+ spaces: new SpaceRepository(db),
24
+ assets: new AssetRepository(db),
25
+ assetUsages: new AssetUsageRepository(db),
26
+ users: new UserRepository(db),
27
+ };
28
+ }
@@ -0,0 +1,78 @@
1
+ import { type Loose, ManabloxError } from '@manablox/core';
2
+ import { eq } from 'drizzle-orm';
3
+ import type { Database } from '../client.js';
4
+ import { type SpaceRow, spaces } from '../schema.js';
5
+
6
+ export interface SpaceWriteData {
7
+ id?: string | undefined;
8
+ name: string;
9
+ machineName: string;
10
+ description?: string | null | undefined;
11
+ url: string;
12
+ defaultLocale?: string | undefined;
13
+ locales?: string[] | undefined;
14
+ settings?: Record<string, unknown> | undefined;
15
+ }
16
+
17
+ export class SpaceRepository {
18
+ constructor(private readonly db: Database) {}
19
+
20
+ async all(): Promise<SpaceRow[]> {
21
+ return this.db.select().from(spaces).orderBy(spaces.name);
22
+ }
23
+
24
+ async findById(id: string): Promise<SpaceRow | null> {
25
+ const rows = await this.db.select().from(spaces).where(eq(spaces.id, id)).limit(1);
26
+ return rows[0] ?? null;
27
+ }
28
+
29
+ async findByMachineName(machineName: string): Promise<SpaceRow | null> {
30
+ const rows = await this.db
31
+ .select()
32
+ .from(spaces)
33
+ .where(eq(spaces.machineName, machineName))
34
+ .limit(1);
35
+ return rows[0] ?? null;
36
+ }
37
+
38
+ async create(data: SpaceWriteData): Promise<SpaceRow> {
39
+ const [row] = await this.db
40
+ .insert(spaces)
41
+ .values({
42
+ ...(data.id ? { id: data.id } : {}),
43
+ name: data.name,
44
+ machineName: data.machineName,
45
+ description: data.description ?? null,
46
+ url: data.url,
47
+ defaultLocale: data.defaultLocale ?? 'en',
48
+ locales: data.locales ?? [data.defaultLocale ?? 'en'],
49
+ settings: data.settings ?? {},
50
+ })
51
+ .returning();
52
+ if (!row) throw new ManabloxError('space.create.failed');
53
+ return row;
54
+ }
55
+
56
+ async update(id: string, data: Loose<SpaceWriteData>): Promise<SpaceRow> {
57
+ const [row] = await this.db
58
+ .update(spaces)
59
+ .set({
60
+ ...(data.name !== undefined ? { name: data.name } : {}),
61
+ ...(data.machineName !== undefined ? { machineName: data.machineName } : {}),
62
+ ...(data.description !== undefined ? { description: data.description } : {}),
63
+ ...(data.url !== undefined ? { url: data.url } : {}),
64
+ ...(data.defaultLocale !== undefined ? { defaultLocale: data.defaultLocale } : {}),
65
+ ...(data.locales !== undefined ? { locales: data.locales } : {}),
66
+ ...(data.settings !== undefined ? { settings: data.settings } : {}),
67
+ updatedAt: new Date(),
68
+ })
69
+ .where(eq(spaces.id, id))
70
+ .returning();
71
+ if (!row) throw ManabloxError.notFound('space.notFound', { id });
72
+ return row;
73
+ }
74
+
75
+ async delete(id: string): Promise<void> {
76
+ await this.db.delete(spaces).where(eq(spaces.id, id));
77
+ }
78
+ }
@@ -0,0 +1,134 @@
1
+ import { ManabloxError } from '@manablox/core';
2
+ import { and, desc, eq, inArray, sql } from 'drizzle-orm';
3
+ import type { Database } from '../client.js';
4
+ import { type MembershipRow, memberships, type UserRow, users } from '../schema.js';
5
+ import type { Paginated, Pagination } from './content.js';
6
+
7
+ export type SpaceRole = 'owner' | 'admin' | 'editor' | 'author' | 'viewer';
8
+
9
+ export class UserRepository {
10
+ constructor(private readonly db: Database) {}
11
+
12
+ async findById(id: string): Promise<UserRow | null> {
13
+ const rows = await this.db.select().from(users).where(eq(users.id, id)).limit(1);
14
+ return rows[0] ?? null;
15
+ }
16
+
17
+ async findManyByIds(ids: string[]): Promise<UserRow[]> {
18
+ if (ids.length === 0) return [];
19
+ return this.db.select().from(users).where(inArray(users.id, ids));
20
+ }
21
+
22
+ async findByEmail(email: string): Promise<UserRow | null> {
23
+ const rows = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
24
+ return rows[0] ?? null;
25
+ }
26
+
27
+ async list(pagination: Pagination, search?: string): Promise<Paginated<UserRow>> {
28
+ const where = search
29
+ ? sql`${users.email} ilike ${`%${search}%`} or ${users.name} ilike ${`%${search}%`}`
30
+ : undefined;
31
+
32
+ const [items, counted] = await Promise.all([
33
+ this.db
34
+ .select()
35
+ .from(users)
36
+ .where(where)
37
+ .orderBy(desc(users.createdAt))
38
+ .limit(pagination.limit)
39
+ .offset(pagination.offset),
40
+ this.db.select({ count: sql<number>`count(*)::int` }).from(users).where(where),
41
+ ]);
42
+
43
+ return {
44
+ items,
45
+ total: counted[0]?.count ?? 0,
46
+ limit: pagination.limit,
47
+ offset: pagination.offset,
48
+ };
49
+ }
50
+
51
+ async count(): Promise<number> {
52
+ const rows = await this.db.select({ count: sql<number>`count(*)::int` }).from(users);
53
+ return rows[0]?.count ?? 0;
54
+ }
55
+
56
+ async setRole(id: string, role: string): Promise<UserRow> {
57
+ const [row] = await this.db
58
+ .update(users)
59
+ .set({ role, updatedAt: new Date() })
60
+ .where(eq(users.id, id))
61
+ .returning();
62
+ if (!row) throw ManabloxError.notFound('user.notFound', { id });
63
+ return row;
64
+ }
65
+
66
+ // --- space membership -----------------------------------------------------
67
+
68
+ /**
69
+ * Authoritative role plus space memberships in one query.
70
+ *
71
+ * Read on every authenticated request rather than trusting the role embedded in the
72
+ * session: better-auth caches the session payload (five minutes by default), so a
73
+ * promotion or demotion would otherwise not take effect until that cache expired.
74
+ */
75
+ async principal(
76
+ userId: string,
77
+ ): Promise<{ role: string; banned: boolean; spaces: Record<string, SpaceRole> } | null> {
78
+ const rows = await this.db
79
+ .select({
80
+ role: users.role,
81
+ banned: users.banned,
82
+ spaceId: memberships.spaceId,
83
+ spaceRole: memberships.role,
84
+ })
85
+ .from(users)
86
+ .leftJoin(memberships, eq(memberships.userId, users.id))
87
+ .where(eq(users.id, userId));
88
+
89
+ const first = rows[0];
90
+ if (!first) return null;
91
+
92
+ const spaces: Record<string, SpaceRole> = {};
93
+ for (const row of rows) {
94
+ if (row.spaceId && row.spaceRole) spaces[row.spaceId] = row.spaceRole;
95
+ }
96
+
97
+ return { role: first.role, banned: first.banned, spaces };
98
+ }
99
+
100
+ async memberships(userId: string): Promise<MembershipRow[]> {
101
+ return this.db.select().from(memberships).where(eq(memberships.userId, userId));
102
+ }
103
+
104
+ async membersOf(spaceId: string): Promise<Array<MembershipRow & { user: UserRow }>> {
105
+ const rows = await this.db
106
+ .select({ membership: memberships, user: users })
107
+ .from(memberships)
108
+ .innerJoin(users, eq(users.id, memberships.userId))
109
+ .where(eq(memberships.spaceId, spaceId));
110
+ return rows.map((row) => ({ ...row.membership, user: row.user }));
111
+ }
112
+
113
+ async roleIn(userId: string, spaceId: string): Promise<SpaceRole | null> {
114
+ const rows = await this.db
115
+ .select({ role: memberships.role })
116
+ .from(memberships)
117
+ .where(and(eq(memberships.userId, userId), eq(memberships.spaceId, spaceId)))
118
+ .limit(1);
119
+ return rows[0]?.role ?? null;
120
+ }
121
+
122
+ async grant(userId: string, spaceId: string, role: SpaceRole): Promise<void> {
123
+ await this.db
124
+ .insert(memberships)
125
+ .values({ userId, spaceId, role })
126
+ .onConflictDoUpdate({ target: [memberships.userId, memberships.spaceId], set: { role } });
127
+ }
128
+
129
+ async revoke(userId: string, spaceId: string): Promise<void> {
130
+ await this.db
131
+ .delete(memberships)
132
+ .where(and(eq(memberships.userId, userId), eq(memberships.spaceId, spaceId)));
133
+ }
134
+ }