@vobase/core 0.9.0 → 0.11.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.
- package/package.json +9 -10
- package/src/__tests__/drizzle-introspection.test.ts +77 -0
- package/src/__tests__/e2e.test.ts +225 -0
- package/src/__tests__/permissions.test.ts +157 -0
- package/src/__tests__/rpc-types.test.ts +92 -0
- package/src/app.test.ts +99 -0
- package/src/app.ts +178 -0
- package/src/audit.test.ts +126 -0
- package/src/auth.test.ts +74 -0
- package/src/contracts/auth.ts +37 -0
- package/{dist/contracts/module.d.ts → src/contracts/module.ts} +6 -6
- package/src/contracts/notify.ts +47 -0
- package/src/contracts/permissions.ts +10 -0
- package/src/contracts/storage.ts +61 -0
- package/src/ctx.test.ts +162 -0
- package/src/ctx.ts +64 -0
- package/src/db/client.test.ts +75 -0
- package/src/db/client.ts +15 -0
- package/src/db/helpers.test.ts +147 -0
- package/src/db/helpers.ts +51 -0
- package/src/db/index.ts +8 -0
- package/{dist/index.d.ts → src/index.ts} +105 -6
- package/src/infra/circuit-breaker.test.ts +74 -0
- package/src/infra/circuit-breaker.ts +57 -0
- package/src/infra/errors.test.ts +175 -0
- package/src/infra/errors.ts +64 -0
- package/src/infra/http-client.test.ts +482 -0
- package/src/infra/http-client.ts +221 -0
- package/src/infra/index.ts +35 -0
- package/src/infra/job.test.ts +85 -0
- package/src/infra/job.ts +94 -0
- package/src/infra/logger.test.ts +65 -0
- package/src/infra/logger.ts +18 -0
- package/src/infra/queue.test.ts +46 -0
- package/src/infra/queue.ts +147 -0
- package/src/infra/throw-proxy.test.ts +34 -0
- package/src/infra/throw-proxy.ts +17 -0
- package/src/infra/webhooks-schema.ts +17 -0
- package/src/infra/webhooks.test.ts +364 -0
- package/src/infra/webhooks.ts +146 -0
- package/src/mcp/auth.test.ts +129 -0
- package/src/mcp/crud.test.ts +128 -0
- package/src/mcp/crud.ts +171 -0
- package/{dist/mcp/index.d.ts → src/mcp/index.ts} +0 -1
- package/src/mcp/server.test.ts +153 -0
- package/src/mcp/server.ts +178 -0
- package/src/middleware/audit.test.ts +169 -0
- package/src/module-registry.ts +18 -0
- package/src/module.test.ts +168 -0
- package/src/module.ts +111 -0
- package/src/modules/audit/index.ts +18 -0
- package/src/modules/audit/middleware.ts +33 -0
- package/src/modules/audit/schema.ts +35 -0
- package/src/modules/audit/track-changes.ts +70 -0
- package/src/modules/auth/audit-hooks.ts +74 -0
- package/src/modules/auth/index.ts +101 -0
- package/src/modules/auth/middleware.ts +51 -0
- package/src/modules/auth/permissions.ts +46 -0
- package/src/modules/auth/schema.ts +184 -0
- package/src/modules/credentials/encrypt.ts +95 -0
- package/src/modules/credentials/index.ts +15 -0
- package/src/modules/credentials/schema.ts +10 -0
- package/src/modules/notify/index.ts +90 -0
- package/src/modules/notify/notify.test.ts +145 -0
- package/src/modules/notify/providers/resend.ts +47 -0
- package/src/modules/notify/providers/smtp.ts +117 -0
- package/src/modules/notify/providers/waba.ts +82 -0
- package/src/modules/notify/schema.ts +27 -0
- package/src/modules/notify/service.ts +93 -0
- package/src/modules/sequences/index.ts +15 -0
- package/src/modules/sequences/next-sequence.ts +48 -0
- package/src/modules/sequences/schema.ts +12 -0
- package/src/modules/storage/index.ts +44 -0
- package/src/modules/storage/providers/local.ts +124 -0
- package/src/modules/storage/providers/s3.ts +83 -0
- package/src/modules/storage/routes.ts +76 -0
- package/src/modules/storage/schema.ts +26 -0
- package/src/modules/storage/service.ts +202 -0
- package/src/modules/storage/storage.test.ts +225 -0
- package/src/schemas.test.ts +44 -0
- package/src/schemas.ts +63 -0
- package/src/sequence.test.ts +56 -0
- package/dist/app.d.ts +0 -37
- package/dist/app.d.ts.map +0 -1
- package/dist/contracts/auth.d.ts +0 -35
- package/dist/contracts/auth.d.ts.map +0 -1
- package/dist/contracts/module.d.ts.map +0 -1
- package/dist/contracts/notify.d.ts +0 -46
- package/dist/contracts/notify.d.ts.map +0 -1
- package/dist/contracts/permissions.d.ts +0 -10
- package/dist/contracts/permissions.d.ts.map +0 -1
- package/dist/contracts/storage.d.ts +0 -54
- package/dist/contracts/storage.d.ts.map +0 -1
- package/dist/ctx.d.ts +0 -40
- package/dist/ctx.d.ts.map +0 -1
- package/dist/db/client.d.ts +0 -4
- package/dist/db/client.d.ts.map +0 -1
- package/dist/db/helpers.d.ts +0 -26
- package/dist/db/helpers.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -3
- package/dist/db/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -89026
- package/dist/infra/circuit-breaker.d.ts +0 -17
- package/dist/infra/circuit-breaker.d.ts.map +0 -1
- package/dist/infra/errors.d.ts +0 -26
- package/dist/infra/errors.d.ts.map +0 -1
- package/dist/infra/http-client.d.ts +0 -31
- package/dist/infra/http-client.d.ts.map +0 -1
- package/dist/infra/index.d.ts +0 -11
- package/dist/infra/index.d.ts.map +0 -1
- package/dist/infra/job.d.ts +0 -14
- package/dist/infra/job.d.ts.map +0 -1
- package/dist/infra/logger.d.ts +0 -7
- package/dist/infra/logger.d.ts.map +0 -1
- package/dist/infra/queue.d.ts +0 -18
- package/dist/infra/queue.d.ts.map +0 -1
- package/dist/infra/throw-proxy.d.ts +0 -7
- package/dist/infra/throw-proxy.d.ts.map +0 -1
- package/dist/infra/webhooks-schema.d.ts +0 -60
- package/dist/infra/webhooks-schema.d.ts.map +0 -1
- package/dist/infra/webhooks.d.ts +0 -46
- package/dist/infra/webhooks.d.ts.map +0 -1
- package/dist/mcp/crud.d.ts +0 -12
- package/dist/mcp/crud.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts +0 -10
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/module-registry.d.ts +0 -3
- package/dist/module-registry.d.ts.map +0 -1
- package/dist/module.d.ts +0 -29
- package/dist/module.d.ts.map +0 -1
- package/dist/modules/audit/index.d.ts +0 -5
- package/dist/modules/audit/index.d.ts.map +0 -1
- package/dist/modules/audit/middleware.d.ts +0 -3
- package/dist/modules/audit/middleware.d.ts.map +0 -1
- package/dist/modules/audit/schema.d.ts +0 -247
- package/dist/modules/audit/schema.d.ts.map +0 -1
- package/dist/modules/audit/track-changes.d.ts +0 -3
- package/dist/modules/audit/track-changes.d.ts.map +0 -1
- package/dist/modules/auth/audit-hooks.d.ts +0 -6
- package/dist/modules/auth/audit-hooks.d.ts.map +0 -1
- package/dist/modules/auth/index.d.ts +0 -17
- package/dist/modules/auth/index.d.ts.map +0 -1
- package/dist/modules/auth/middleware.d.ts +0 -15
- package/dist/modules/auth/middleware.d.ts.map +0 -1
- package/dist/modules/auth/permissions.d.ts +0 -5
- package/dist/modules/auth/permissions.d.ts.map +0 -1
- package/dist/modules/auth/schema.d.ts +0 -2519
- package/dist/modules/auth/schema.d.ts.map +0 -1
- package/dist/modules/credentials/encrypt.d.ts +0 -12
- package/dist/modules/credentials/encrypt.d.ts.map +0 -1
- package/dist/modules/credentials/index.d.ts +0 -4
- package/dist/modules/credentials/index.d.ts.map +0 -1
- package/dist/modules/credentials/schema.d.ts +0 -56
- package/dist/modules/credentials/schema.d.ts.map +0 -1
- package/dist/modules/notify/index.d.ts +0 -36
- package/dist/modules/notify/index.d.ts.map +0 -1
- package/dist/modules/notify/providers/resend.d.ts +0 -7
- package/dist/modules/notify/providers/resend.d.ts.map +0 -1
- package/dist/modules/notify/providers/smtp.d.ts +0 -18
- package/dist/modules/notify/providers/smtp.d.ts.map +0 -1
- package/dist/modules/notify/providers/waba.d.ts +0 -12
- package/dist/modules/notify/providers/waba.d.ts.map +0 -1
- package/dist/modules/notify/schema.d.ts +0 -337
- package/dist/modules/notify/schema.d.ts.map +0 -1
- package/dist/modules/notify/service.d.ts +0 -22
- package/dist/modules/notify/service.d.ts.map +0 -1
- package/dist/modules/sequences/index.d.ts +0 -4
- package/dist/modules/sequences/index.d.ts.map +0 -1
- package/dist/modules/sequences/next-sequence.d.ts +0 -8
- package/dist/modules/sequences/next-sequence.d.ts.map +0 -1
- package/dist/modules/sequences/schema.d.ts +0 -72
- package/dist/modules/sequences/schema.d.ts.map +0 -1
- package/dist/modules/storage/index.d.ts +0 -24
- package/dist/modules/storage/index.d.ts.map +0 -1
- package/dist/modules/storage/providers/local.d.ts +0 -3
- package/dist/modules/storage/providers/local.d.ts.map +0 -1
- package/dist/modules/storage/providers/s3.d.ts +0 -3
- package/dist/modules/storage/providers/s3.d.ts.map +0 -1
- package/dist/modules/storage/routes.d.ts +0 -4
- package/dist/modules/storage/routes.d.ts.map +0 -1
- package/dist/modules/storage/schema.d.ts +0 -273
- package/dist/modules/storage/schema.d.ts.map +0 -1
- package/dist/modules/storage/service.d.ts +0 -35
- package/dist/modules/storage/service.d.ts.map +0 -1
- package/dist/schemas.d.ts +0 -19
- package/dist/schemas.d.ts.map +0 -1
package/src/ctx.test.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
contextMiddleware,
|
|
6
|
+
getCtx,
|
|
7
|
+
type VobaseCtx,
|
|
8
|
+
type VobaseUser,
|
|
9
|
+
} from './ctx';
|
|
10
|
+
import type { VobaseDb } from './db';
|
|
11
|
+
import type { HttpClient } from './infra/http-client';
|
|
12
|
+
import type { Scheduler } from './infra/queue';
|
|
13
|
+
import type { NotifyService } from './modules/notify/service';
|
|
14
|
+
import type { StorageService } from './modules/storage/service';
|
|
15
|
+
|
|
16
|
+
const db = {} as VobaseDb;
|
|
17
|
+
const scheduler: Scheduler = {
|
|
18
|
+
async add() {},
|
|
19
|
+
};
|
|
20
|
+
const storage: StorageService = {
|
|
21
|
+
bucket() {
|
|
22
|
+
throw new Error('not implemented');
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const notify: NotifyService = {
|
|
26
|
+
email: { send: async () => ({ success: true }) },
|
|
27
|
+
whatsapp: { send: async () => ({ success: true }) },
|
|
28
|
+
};
|
|
29
|
+
const mockResponse = { ok: true, status: 200, headers: new Headers(), data: null, raw: new Response() };
|
|
30
|
+
const http: HttpClient = {
|
|
31
|
+
fetch: async () => mockResponse,
|
|
32
|
+
get: async () => mockResponse,
|
|
33
|
+
post: async () => mockResponse,
|
|
34
|
+
put: async () => mockResponse,
|
|
35
|
+
delete: async () => mockResponse,
|
|
36
|
+
} as HttpClient;
|
|
37
|
+
|
|
38
|
+
function expectType<T>(_value: T): void {}
|
|
39
|
+
|
|
40
|
+
describe('ctx helpers', () => {
|
|
41
|
+
it('getCtx(c) returns all properties when set', async () => {
|
|
42
|
+
const user: VobaseUser = {
|
|
43
|
+
id: 'user_123',
|
|
44
|
+
email: 'user@example.com',
|
|
45
|
+
name: 'Test User',
|
|
46
|
+
role: 'admin',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const app = new Hono();
|
|
50
|
+
app.use('*', async (c, next) => {
|
|
51
|
+
c.set('db', db);
|
|
52
|
+
c.set('scheduler', scheduler);
|
|
53
|
+
c.set('storage', storage);
|
|
54
|
+
c.set('notify', notify);
|
|
55
|
+
c.set('http', http);
|
|
56
|
+
c.set('user', user);
|
|
57
|
+
await next();
|
|
58
|
+
});
|
|
59
|
+
app.get('/', (c) => {
|
|
60
|
+
const ctx = getCtx(c);
|
|
61
|
+
return c.json({
|
|
62
|
+
hasDb: ctx.db === db,
|
|
63
|
+
hasScheduler: ctx.scheduler === scheduler,
|
|
64
|
+
hasStorage: ctx.storage === storage,
|
|
65
|
+
hasNotify: ctx.notify === notify,
|
|
66
|
+
hasHttp: ctx.http === http,
|
|
67
|
+
user: ctx.user,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const response = await app.request('http://localhost/');
|
|
72
|
+
const body = (await response.json()) as {
|
|
73
|
+
hasDb: boolean;
|
|
74
|
+
hasScheduler: boolean;
|
|
75
|
+
hasStorage: boolean;
|
|
76
|
+
hasNotify: boolean;
|
|
77
|
+
hasHttp: boolean;
|
|
78
|
+
user: VobaseUser | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
expect(response.status).toBe(200);
|
|
82
|
+
expect(body.hasDb).toBe(true);
|
|
83
|
+
expect(body.hasScheduler).toBe(true);
|
|
84
|
+
expect(body.hasStorage).toBe(true);
|
|
85
|
+
expect(body.hasNotify).toBe(true);
|
|
86
|
+
expect(body.hasHttp).toBe(true);
|
|
87
|
+
expect(body.user).toEqual(user);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns null user when session middleware did not set user', async () => {
|
|
91
|
+
const app = new Hono();
|
|
92
|
+
app.use('*', contextMiddleware({ db, scheduler, storage, notify, http }));
|
|
93
|
+
app.get('/', (c) => c.json({ user: getCtx(c).user }));
|
|
94
|
+
|
|
95
|
+
const response = await app.request('http://localhost/');
|
|
96
|
+
const body = (await response.json()) as { user: VobaseUser | null };
|
|
97
|
+
|
|
98
|
+
expect(response.status).toBe(200);
|
|
99
|
+
expect(body.user).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('contextMiddleware sets db, scheduler, storage, and http', async () => {
|
|
103
|
+
const app = new Hono();
|
|
104
|
+
app.use('*', contextMiddleware({ db, scheduler, storage, notify, http }));
|
|
105
|
+
app.get('/', (c) => {
|
|
106
|
+
return c.json({
|
|
107
|
+
hasDb: c.get('db') === db,
|
|
108
|
+
hasScheduler: c.get('scheduler') === scheduler,
|
|
109
|
+
hasStorage: c.get('storage') === storage,
|
|
110
|
+
hasHttp: c.get('http') === http,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const response = await app.request('http://localhost/');
|
|
115
|
+
const body = (await response.json()) as {
|
|
116
|
+
hasDb: boolean;
|
|
117
|
+
hasScheduler: boolean;
|
|
118
|
+
hasStorage: boolean;
|
|
119
|
+
hasHttp: boolean;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(200);
|
|
123
|
+
expect(body.hasDb).toBe(true);
|
|
124
|
+
expect(body.hasScheduler).toBe(true);
|
|
125
|
+
expect(body.hasStorage).toBe(true);
|
|
126
|
+
expect(body.hasHttp).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('getCtx(c) includes http client', async () => {
|
|
130
|
+
const app = new Hono();
|
|
131
|
+
app.use('*', contextMiddleware({ db, scheduler, storage, notify, http }));
|
|
132
|
+
app.get('/', (c) => {
|
|
133
|
+
const ctx = getCtx(c);
|
|
134
|
+
return c.json({ hasHttp: ctx.http === http });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const response = await app.request('http://localhost/');
|
|
138
|
+
const body = (await response.json()) as { hasHttp: boolean };
|
|
139
|
+
|
|
140
|
+
expect(response.status).toBe(200);
|
|
141
|
+
expect(body.hasHttp).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('exposes correctly typed properties on VobaseCtx', async () => {
|
|
145
|
+
const app = new Hono();
|
|
146
|
+
app.use('*', contextMiddleware({ db, scheduler, storage, notify, http }));
|
|
147
|
+
app.get('/', (c) => {
|
|
148
|
+
const ctx = getCtx(c);
|
|
149
|
+
expectType<VobaseCtx>(ctx);
|
|
150
|
+
expectType<VobaseDb>(ctx.db);
|
|
151
|
+
expectType<VobaseUser | null>(ctx.user);
|
|
152
|
+
expectType<Scheduler>(ctx.scheduler);
|
|
153
|
+
expectType<StorageService>(ctx.storage);
|
|
154
|
+
expectType<NotifyService>(ctx.notify);
|
|
155
|
+
expectType<HttpClient>(ctx.http);
|
|
156
|
+
return c.text('ok');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const response = await app.request('http://localhost/');
|
|
160
|
+
expect(response.status).toBe(200);
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/ctx.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { createMiddleware } from 'hono/factory';
|
|
3
|
+
|
|
4
|
+
import type { VobaseDb } from './db';
|
|
5
|
+
import type { HttpClient } from './infra/http-client';
|
|
6
|
+
import type { Scheduler } from './infra/queue';
|
|
7
|
+
import type { NotifyService } from './modules/notify/service';
|
|
8
|
+
import type { StorageService } from './modules/storage/service';
|
|
9
|
+
|
|
10
|
+
export interface VobaseUser {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name: string;
|
|
14
|
+
role: string;
|
|
15
|
+
/** Set when the user has an active organization (better-auth organization plugin) */
|
|
16
|
+
activeOrganizationId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VobaseCtx {
|
|
20
|
+
db: VobaseDb;
|
|
21
|
+
user: VobaseUser | null;
|
|
22
|
+
scheduler: Scheduler;
|
|
23
|
+
storage: StorageService;
|
|
24
|
+
notify: NotifyService;
|
|
25
|
+
http: HttpClient;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module 'hono' {
|
|
29
|
+
interface ContextVariableMap {
|
|
30
|
+
db: VobaseDb;
|
|
31
|
+
scheduler: Scheduler;
|
|
32
|
+
storage: StorageService;
|
|
33
|
+
notify: NotifyService;
|
|
34
|
+
http: HttpClient;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function contextMiddleware(deps: {
|
|
39
|
+
db: VobaseDb;
|
|
40
|
+
scheduler: Scheduler;
|
|
41
|
+
storage: StorageService;
|
|
42
|
+
notify: NotifyService;
|
|
43
|
+
http: HttpClient;
|
|
44
|
+
}) {
|
|
45
|
+
return createMiddleware(async (c, next) => {
|
|
46
|
+
c.set('db', deps.db);
|
|
47
|
+
c.set('scheduler', deps.scheduler);
|
|
48
|
+
c.set('storage', deps.storage);
|
|
49
|
+
c.set('notify', deps.notify);
|
|
50
|
+
c.set('http', deps.http);
|
|
51
|
+
await next();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getCtx(c: Context): VobaseCtx {
|
|
56
|
+
return {
|
|
57
|
+
db: c.get('db'),
|
|
58
|
+
user: c.get('user') ?? null,
|
|
59
|
+
scheduler: c.get('scheduler'),
|
|
60
|
+
storage: c.get('storage'),
|
|
61
|
+
notify: c.get('notify'),
|
|
62
|
+
http: c.get('http'),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import type { Database } from 'bun:sqlite';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
import { createDatabase } from './client';
|
|
8
|
+
|
|
9
|
+
type DbWithClient = ReturnType<typeof createDatabase> & { $client: Database };
|
|
10
|
+
|
|
11
|
+
const tempDbPaths = new Set<string>();
|
|
12
|
+
|
|
13
|
+
function getPragmaValue(db: DbWithClient, pragma: string): string {
|
|
14
|
+
const result = db.$client.query(`PRAGMA ${pragma}`).get() as Record<
|
|
15
|
+
string,
|
|
16
|
+
unknown
|
|
17
|
+
>;
|
|
18
|
+
return String(Object.values(result)[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function closeDatabase(db: DbWithClient): void {
|
|
22
|
+
db.$client.close();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
for (const dbPath of tempDbPaths) {
|
|
27
|
+
rmSync(dbPath, { force: true });
|
|
28
|
+
}
|
|
29
|
+
tempDbPaths.clear();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('createDatabase', () => {
|
|
33
|
+
it('creates an in-memory drizzle database client', () => {
|
|
34
|
+
const db = createDatabase(':memory:') as DbWithClient;
|
|
35
|
+
|
|
36
|
+
expect(db).toBeDefined();
|
|
37
|
+
expect(db.$client).toBeDefined();
|
|
38
|
+
|
|
39
|
+
closeDatabase(db);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sets PRAGMA busy_timeout to 5000', () => {
|
|
43
|
+
const db = createDatabase(':memory:') as DbWithClient;
|
|
44
|
+
|
|
45
|
+
expect(getPragmaValue(db, 'busy_timeout')).toBe('5000');
|
|
46
|
+
|
|
47
|
+
closeDatabase(db);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('sets PRAGMA synchronous to NORMAL (1)', () => {
|
|
51
|
+
const db = createDatabase(':memory:') as DbWithClient;
|
|
52
|
+
|
|
53
|
+
expect(getPragmaValue(db, 'synchronous')).toBe('1');
|
|
54
|
+
|
|
55
|
+
closeDatabase(db);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets PRAGMA foreign_keys to ON (1)', () => {
|
|
59
|
+
const db = createDatabase(':memory:') as DbWithClient;
|
|
60
|
+
|
|
61
|
+
expect(getPragmaValue(db, 'foreign_keys')).toBe('1');
|
|
62
|
+
|
|
63
|
+
closeDatabase(db);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('sets PRAGMA journal_mode to WAL for file databases', () => {
|
|
67
|
+
const dbPath = resolve(tmpdir(), `vobase-client-${Date.now()}.db`);
|
|
68
|
+
tempDbPaths.add(dbPath);
|
|
69
|
+
const db = createDatabase(dbPath) as DbWithClient;
|
|
70
|
+
|
|
71
|
+
expect(getPragmaValue(db, 'journal_mode')).toBe('wal');
|
|
72
|
+
|
|
73
|
+
closeDatabase(db);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/db/client.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
3
|
+
|
|
4
|
+
export type VobaseDb = ReturnType<typeof drizzle>;
|
|
5
|
+
|
|
6
|
+
export function createDatabase(dbPath: string): VobaseDb {
|
|
7
|
+
const sqlite = new Database(dbPath);
|
|
8
|
+
|
|
9
|
+
sqlite.run('PRAGMA journal_mode=WAL');
|
|
10
|
+
sqlite.run('PRAGMA busy_timeout=5000');
|
|
11
|
+
sqlite.run('PRAGMA synchronous=NORMAL');
|
|
12
|
+
sqlite.run('PRAGMA foreign_keys=ON');
|
|
13
|
+
|
|
14
|
+
return drizzle({ client: sqlite });
|
|
15
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createNanoid,
|
|
5
|
+
DEFAULT_COLUMNS,
|
|
6
|
+
NANOID_ALPHABET,
|
|
7
|
+
NANOID_LENGTH,
|
|
8
|
+
nanoidPrimaryKey,
|
|
9
|
+
} from './helpers';
|
|
10
|
+
|
|
11
|
+
// Drizzle marks `.config` as protected; bracket access bypasses this for test inspection
|
|
12
|
+
function getConfig(col: unknown): Record<string, unknown> {
|
|
13
|
+
return (col as Record<string, Record<string, unknown>>).config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('nanoid helpers', () => {
|
|
17
|
+
describe('NANOID_LENGTH constants', () => {
|
|
18
|
+
it('should have SHORT, DEFAULT, and LONG lengths defined', () => {
|
|
19
|
+
expect(NANOID_LENGTH.SHORT).toBe(8);
|
|
20
|
+
expect(NANOID_LENGTH.DEFAULT).toBe(12);
|
|
21
|
+
expect(NANOID_LENGTH.LONG).toBe(16);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('NANOID_ALPHABET', () => {
|
|
26
|
+
it('should only contain lowercase alphanumeric characters', () => {
|
|
27
|
+
expect(NANOID_ALPHABET).toBe('0123456789abcdefghijklmnopqrstuvwxyz');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('createNanoid()', () => {
|
|
32
|
+
it('should generate IDs of the correct length', () => {
|
|
33
|
+
const generateShort = createNanoid(NANOID_LENGTH.SHORT);
|
|
34
|
+
const generateDefault = createNanoid(NANOID_LENGTH.DEFAULT);
|
|
35
|
+
const generateLong = createNanoid(NANOID_LENGTH.LONG);
|
|
36
|
+
|
|
37
|
+
expect(generateShort().length).toBe(8);
|
|
38
|
+
expect(generateDefault().length).toBe(12);
|
|
39
|
+
expect(generateLong().length).toBe(16);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should generate IDs using only the alphabet', () => {
|
|
43
|
+
const generate = createNanoid(NANOID_LENGTH.DEFAULT);
|
|
44
|
+
const id = generate();
|
|
45
|
+
|
|
46
|
+
for (const char of id) {
|
|
47
|
+
expect(NANOID_ALPHABET.includes(char)).toBe(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should cache generators and reuse them', () => {
|
|
52
|
+
const gen1 = createNanoid(NANOID_LENGTH.DEFAULT);
|
|
53
|
+
const gen2 = createNanoid(NANOID_LENGTH.DEFAULT);
|
|
54
|
+
|
|
55
|
+
// Should be the same function instance (cached)
|
|
56
|
+
expect(gen1).toBe(gen2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should generate unique IDs', () => {
|
|
60
|
+
const generate = createNanoid(NANOID_LENGTH.DEFAULT);
|
|
61
|
+
const ids = new Set();
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < 1000; i++) {
|
|
64
|
+
ids.add(generate());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(ids.size).toBe(1000);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should use default length when not specified', () => {
|
|
71
|
+
const generate = createNanoid();
|
|
72
|
+
expect(generate().length).toBe(NANOID_LENGTH.DEFAULT);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('nanoidPrimaryKey()', () => {
|
|
77
|
+
it('should create a text column named "id"', () => {
|
|
78
|
+
const column = nanoidPrimaryKey();
|
|
79
|
+
expect(getConfig(column).name).toBe('id');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should be a primary key', () => {
|
|
83
|
+
const column = nanoidPrimaryKey();
|
|
84
|
+
expect(getConfig(column).primaryKey).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should have a default function', () => {
|
|
88
|
+
const column = nanoidPrimaryKey();
|
|
89
|
+
expect(typeof column.$default).toBe('function');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should generate valid nanoid', () => {
|
|
93
|
+
// createNanoid() returns a generator function
|
|
94
|
+
const generator = createNanoid();
|
|
95
|
+
const id = generator();
|
|
96
|
+
|
|
97
|
+
expect(id).toBeDefined();
|
|
98
|
+
expect(typeof id).toBe('string');
|
|
99
|
+
expect(id.length).toBe(NANOID_LENGTH.DEFAULT);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should support custom lengths', () => {
|
|
103
|
+
const columnShort = nanoidPrimaryKey(NANOID_LENGTH.SHORT);
|
|
104
|
+
const columnLong = nanoidPrimaryKey(NANOID_LENGTH.LONG);
|
|
105
|
+
|
|
106
|
+
expect(getConfig(columnShort).name).toBe('id');
|
|
107
|
+
expect(getConfig(columnLong).name).toBe('id');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('DEFAULT_COLUMNS', () => {
|
|
112
|
+
it('should have createdAt and updatedAt columns', () => {
|
|
113
|
+
expect(DEFAULT_COLUMNS.createdAt).toBeDefined();
|
|
114
|
+
expect(DEFAULT_COLUMNS.updatedAt).toBeDefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('createdAt should be an integer column with timestamp_ms mode', () => {
|
|
118
|
+
const col = DEFAULT_COLUMNS.createdAt;
|
|
119
|
+
expect(getConfig(col).name).toBe('created_at');
|
|
120
|
+
expect(getConfig(col).mode).toBe('timestamp_ms');
|
|
121
|
+
expect(getConfig(col).hasDefault).toBe(true);
|
|
122
|
+
expect(getConfig(col).notNull).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('updatedAt should be an integer column with timestamp_ms mode', () => {
|
|
126
|
+
const col = DEFAULT_COLUMNS.updatedAt;
|
|
127
|
+
expect(getConfig(col).name).toBe('updated_at');
|
|
128
|
+
expect(getConfig(col).mode).toBe('timestamp_ms');
|
|
129
|
+
expect(getConfig(col).hasDefault).toBe(true);
|
|
130
|
+
expect(getConfig(col).notNull).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('timestamps should have default functions', () => {
|
|
134
|
+
expect(typeof DEFAULT_COLUMNS.createdAt.$default).toBe('function');
|
|
135
|
+
expect(typeof DEFAULT_COLUMNS.updatedAt.$default).toBe('function');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('updatedAt should have onUpdate function', () => {
|
|
139
|
+
expect(typeof DEFAULT_COLUMNS.updatedAt.$onUpdate).toBe('function');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('timestamp columns should use integer data type', () => {
|
|
143
|
+
expect(getConfig(DEFAULT_COLUMNS.createdAt).dataType).toBe('object date');
|
|
144
|
+
expect(getConfig(DEFAULT_COLUMNS.updatedAt).dataType).toBe('object date');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { integer, text } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import { customAlphabet } from 'nanoid';
|
|
3
|
+
|
|
4
|
+
export const NANOID_LENGTH = { SHORT: 8, DEFAULT: 12, LONG: 16 } as const;
|
|
5
|
+
export const NANOID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
6
|
+
|
|
7
|
+
// Cache generators to avoid recreating
|
|
8
|
+
const nanoidGenerators = new Map<number, () => string>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create or retrieve a cached nanoid generator with the specified length.
|
|
12
|
+
* Uses a custom alphabet of lowercase alphanumeric characters.
|
|
13
|
+
*/
|
|
14
|
+
export function createNanoid(
|
|
15
|
+
length: number = NANOID_LENGTH.DEFAULT,
|
|
16
|
+
): () => string {
|
|
17
|
+
if (!nanoidGenerators.has(length)) {
|
|
18
|
+
nanoidGenerators.set(length, customAlphabet(NANOID_ALPHABET, length));
|
|
19
|
+
}
|
|
20
|
+
const generator = nanoidGenerators.get(length);
|
|
21
|
+
|
|
22
|
+
if (!generator) {
|
|
23
|
+
throw new Error(`No nanoid generator for length ${length}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return generator;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a nanoid-based primary key column for SQLite.
|
|
31
|
+
* Generates a 12-character ID by default (customizable).
|
|
32
|
+
*/
|
|
33
|
+
export const nanoidPrimaryKey = (length: number = NANOID_LENGTH.DEFAULT) =>
|
|
34
|
+
text('id')
|
|
35
|
+
.primaryKey()
|
|
36
|
+
.$defaultFn(() => createNanoid(length)());
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default timestamp columns for SQLite using timestamp_ms mode.
|
|
40
|
+
* createdAt: set on insert and never updated
|
|
41
|
+
* updatedAt: set on insert and updated on every row modification
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_COLUMNS = {
|
|
44
|
+
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
|
45
|
+
.notNull()
|
|
46
|
+
.$defaultFn(() => new Date()),
|
|
47
|
+
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
|
48
|
+
.notNull()
|
|
49
|
+
.$defaultFn(() => new Date())
|
|
50
|
+
.$onUpdate(() => new Date()),
|
|
51
|
+
} as const;
|
package/src/db/index.ts
ADDED
|
@@ -1,45 +1,144 @@
|
|
|
1
|
+
// Engine
|
|
1
2
|
export { type CreateAppConfig, createApp } from './app';
|
|
3
|
+
|
|
4
|
+
// Auth Module
|
|
2
5
|
export { createAuthModule, type AuthModuleConfig, type AuthModule } from './modules/auth';
|
|
3
6
|
export { sessionMiddleware, optionalSessionMiddleware } from './modules/auth/middleware';
|
|
7
|
+
|
|
8
|
+
// Auth Schema (tables managed by better-auth)
|
|
9
|
+
export {
|
|
10
|
+
authUser,
|
|
11
|
+
authSession,
|
|
12
|
+
authAccount,
|
|
13
|
+
authVerification,
|
|
14
|
+
authApikey,
|
|
15
|
+
authOrganization,
|
|
16
|
+
authMember,
|
|
17
|
+
authInvitation,
|
|
18
|
+
} from './modules/auth/schema';
|
|
19
|
+
|
|
20
|
+
// RBAC
|
|
21
|
+
export { requireRole, requirePermission, requireOrg } from './modules/auth/permissions';
|
|
22
|
+
export type { Permission, OrganizationContext } from './contracts/permissions';
|
|
23
|
+
|
|
24
|
+
// Contracts
|
|
4
25
|
export type { AuthAdapter, AuthSession, AuthUser } from './contracts/auth';
|
|
5
|
-
export type {
|
|
6
|
-
|
|
26
|
+
export type {
|
|
27
|
+
StorageProvider,
|
|
28
|
+
UploadOptions,
|
|
29
|
+
PresignOptions,
|
|
30
|
+
ListOptions,
|
|
31
|
+
StorageListResult,
|
|
32
|
+
StorageObjectInfo,
|
|
33
|
+
LocalProviderConfig,
|
|
34
|
+
S3ProviderConfig,
|
|
35
|
+
StorageProviderConfig,
|
|
36
|
+
} from './contracts/storage';
|
|
37
|
+
export type {
|
|
38
|
+
EmailProvider,
|
|
39
|
+
EmailMessage,
|
|
40
|
+
EmailAttachment,
|
|
41
|
+
EmailResult,
|
|
42
|
+
WhatsAppProvider,
|
|
43
|
+
WhatsAppMessage,
|
|
44
|
+
WhatsAppResult,
|
|
45
|
+
} from './contracts/notify';
|
|
7
46
|
export type { ModuleInitContext } from './contracts/module';
|
|
47
|
+
|
|
48
|
+
// Context
|
|
8
49
|
export type { VobaseCtx, VobaseUser } from './ctx';
|
|
9
50
|
export { contextMiddleware, getCtx } from './ctx';
|
|
51
|
+
|
|
52
|
+
// Circuit Breaker
|
|
10
53
|
export { CircuitBreaker, type CircuitBreakerOptions } from './infra/circuit-breaker';
|
|
11
|
-
|
|
54
|
+
|
|
55
|
+
// HTTP Client
|
|
56
|
+
export {
|
|
57
|
+
createHttpClient,
|
|
58
|
+
type HttpClient,
|
|
59
|
+
type HttpClientOptions,
|
|
60
|
+
type HttpResponse,
|
|
61
|
+
type RequestOptions,
|
|
62
|
+
} from './infra/http-client';
|
|
63
|
+
|
|
64
|
+
// DB
|
|
12
65
|
export { createDatabase, type VobaseDb } from './db';
|
|
13
|
-
export {
|
|
14
|
-
|
|
66
|
+
export {
|
|
67
|
+
createNanoid,
|
|
68
|
+
DEFAULT_COLUMNS,
|
|
69
|
+
NANOID_ALPHABET,
|
|
70
|
+
NANOID_LENGTH,
|
|
71
|
+
nanoidPrimaryKey,
|
|
72
|
+
} from './db/helpers';
|
|
73
|
+
|
|
74
|
+
// Errors
|
|
75
|
+
export {
|
|
76
|
+
conflict,
|
|
77
|
+
dbBusy,
|
|
78
|
+
ERROR_CODES,
|
|
79
|
+
type ErrorCode,
|
|
80
|
+
errorHandler,
|
|
81
|
+
forbidden,
|
|
82
|
+
notFound,
|
|
83
|
+
unauthorized,
|
|
84
|
+
VobaseError,
|
|
85
|
+
validation,
|
|
86
|
+
} from './infra/errors';
|
|
87
|
+
|
|
88
|
+
// Jobs
|
|
15
89
|
export type { JobDefinition, JobHandler, WorkerOptions } from './infra/job';
|
|
16
90
|
export { createWorker, defineJob } from './infra/job';
|
|
91
|
+
|
|
92
|
+
// Logger
|
|
17
93
|
export { logger } from './infra/logger';
|
|
94
|
+
|
|
95
|
+
// Auth Audit Hooks (re-exported from auth module)
|
|
18
96
|
export { createAuthAuditHooks } from './modules/auth/audit-hooks';
|
|
97
|
+
|
|
98
|
+
// Module
|
|
19
99
|
export type { DefineModuleConfig, VobaseModule } from './module';
|
|
20
100
|
export { defineModule } from './module';
|
|
101
|
+
|
|
102
|
+
// Module Registry
|
|
21
103
|
export { registerModules } from './module-registry';
|
|
104
|
+
|
|
105
|
+
// Queue
|
|
22
106
|
export { createScheduler, type JobOptions, type Scheduler } from './infra/queue';
|
|
107
|
+
|
|
108
|
+
// Built-in Modules: Audit
|
|
23
109
|
export { createAuditModule, auditLog, recordAudits } from './modules/audit';
|
|
24
110
|
export { trackChanges } from './modules/audit/track-changes';
|
|
25
111
|
export { requestAuditMiddleware } from './modules/audit/middleware';
|
|
112
|
+
|
|
113
|
+
// Built-in Modules: Sequences
|
|
26
114
|
export { createSequencesModule, sequences } from './modules/sequences';
|
|
27
115
|
export { nextSequence, type SequenceOptions } from './modules/sequences/next-sequence';
|
|
116
|
+
|
|
117
|
+
// Built-in Modules: Credentials
|
|
28
118
|
export { createCredentialsModule, credentialsTable } from './modules/credentials';
|
|
29
119
|
export { encrypt, decrypt, getCredential, setCredential, deleteCredential } from './modules/credentials/encrypt';
|
|
120
|
+
|
|
121
|
+
// Schemas
|
|
30
122
|
export { getActiveSchemas, type SchemaConfig } from './schemas';
|
|
123
|
+
|
|
124
|
+
// Throw Proxy
|
|
31
125
|
export { createThrowProxy } from './infra/throw-proxy';
|
|
126
|
+
|
|
127
|
+
// Built-in Modules: Storage
|
|
32
128
|
export { createStorageModule, type StorageModuleConfig } from './modules/storage';
|
|
33
129
|
export { createLocalProvider } from './modules/storage/providers/local';
|
|
34
130
|
export { createS3Provider } from './modules/storage/providers/s3';
|
|
35
131
|
export { createStorageRoutes } from './modules/storage/routes';
|
|
36
132
|
export { storageObjects } from './modules/storage/schema';
|
|
37
133
|
export type { StorageService, BucketConfig, BucketHandle, StorageObject, BucketListOptions } from './modules/storage/service';
|
|
134
|
+
|
|
135
|
+
// Built-in Modules: Notify
|
|
38
136
|
export { createNotifyModule, type NotifyModuleConfig } from './modules/notify';
|
|
39
137
|
export { notifyLog } from './modules/notify/schema';
|
|
40
138
|
export { createResendProvider, type ResendConfig } from './modules/notify/providers/resend';
|
|
41
139
|
export { createSmtpProvider, type SmtpConfig } from './modules/notify/providers/smtp';
|
|
42
140
|
export { createWabaProvider, type WabaConfig } from './modules/notify/providers/waba';
|
|
43
141
|
export type { NotifyService, EmailChannel, WhatsAppChannel } from './modules/notify/service';
|
|
142
|
+
|
|
143
|
+
// Webhooks
|
|
44
144
|
export { type WebhookConfig, verifyHmacSignature, createWebhookRoutes, webhookDedup } from './infra/webhooks';
|
|
45
|
-
//# sourceMappingURL=index.d.ts.map
|