@arki/db 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arki/db",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Drizzle ORM repositories and database helpers for ARKI — Postgres + PGlite runtimes, projection builder, branded ID factory, and env-aware connection pooling.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -98,15 +98,21 @@
98
98
  },
99
99
  "files": [
100
100
  "dist/**",
101
+ "src/**",
102
+ "!src/**/*.test.*",
103
+ "!src/**/*.spec.*",
104
+ "!src/**/tests/**",
105
+ "!src/**/test/**",
106
+ "!src/**/__tests__/**",
101
107
  "README.md",
102
108
  "LICENSE",
103
109
  "package.json"
104
110
  ],
105
111
  "dependencies": {
106
- "@arki/assert": "0.0.1",
107
- "@arki/contracts": "0.0.1",
108
- "@arki/env": "0.0.2",
109
- "@arki/log": "0.0.1",
112
+ "@arki/assert": "0.0.2",
113
+ "@arki/contracts": "0.0.2",
114
+ "@arki/env": "0.0.3",
115
+ "@arki/log": "0.0.2",
110
116
  "@electric-sql/pglite": "^0.4.5",
111
117
  "@paralleldrive/cuid2": "^3.3.0",
112
118
  "@t3-oss/env-core": "^0.13.10",
@@ -116,7 +122,7 @@
116
122
  "zod": "4.3.5"
117
123
  },
118
124
  "peerDependencies": {
119
- "@arki/dot": "^0.1.1"
125
+ "@arki/dot": "^0.1.2"
120
126
  },
121
127
  "peerDependenciesMeta": {
122
128
  "@arki/dot": {
package/src/builder.ts ADDED
@@ -0,0 +1,359 @@
1
+ import type { AnyRelations } from 'drizzle-orm';
2
+ import { defineRelations } from 'drizzle-orm';
3
+ import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import type { PoolConfig } from 'pg';
5
+
6
+ import { debugProjection } from './debug.js';
7
+ import { initDbWithOptions } from './init.js';
8
+
9
+ /**
10
+ * Projection middleware context
11
+ */
12
+ export type ProjectionMiddlewareContext = {
13
+ next: () => Promise<void>;
14
+ meta: {
15
+ name: string;
16
+ eventCount: number;
17
+ };
18
+ };
19
+
20
+ /**
21
+ * Projection middleware function
22
+ */
23
+ export type ProjectionMiddleware = (context: ProjectionMiddlewareContext) => Promise<void>;
24
+
25
+ /**
26
+ * Projection handler context
27
+ */
28
+ export type ProjectionHandlerContext<TRepos, TRelations extends AnyRelations = AnyRelations> = {
29
+ repo: TRepos;
30
+ db: NodePgDatabase<TRelations>;
31
+ };
32
+
33
+ /**
34
+ * Projection handler function
35
+ */
36
+ export type ProjectionHandler<TEvent, TRepos, TRelations extends AnyRelations = AnyRelations> = (
37
+ events: TEvent[],
38
+ context: ProjectionHandlerContext<TRepos, TRelations>,
39
+ ) => Promise<void>;
40
+
41
+ /**
42
+ * Projection builder interface
43
+ */
44
+ export type ProjectionBuilder<
45
+ TEvent = never,
46
+ TRepos = never,
47
+ TRelations extends AnyRelations = AnyRelations,
48
+ > = {
49
+ named<TName extends string>(name: TName): ProjectionBuilderWithName<TEvent, TRepos, TRelations, TName>;
50
+ };
51
+
52
+ export type ProjectionBuilderWithName<
53
+ _TEvent,
54
+ TRepos,
55
+ TRelations extends AnyRelations,
56
+ TName extends string,
57
+ > = {
58
+ on<TEventType extends { type: string }>(
59
+ eventTypes: readonly TEventType['type'][] | TEventType['type'][],
60
+ ): ProjectionBuilderWithEvents<TEventType, TRepos, TRelations, TName>;
61
+ };
62
+
63
+ export type ProjectionBuilderWithEvents<
64
+ TEvent,
65
+ TRepos,
66
+ TRelations extends AnyRelations,
67
+ TName extends string,
68
+ > = {
69
+ handle(handler: ProjectionHandler<TEvent, TRepos, TRelations>): ProjectionDefinition<TName>;
70
+ };
71
+
72
+ /**
73
+ * Emmett-compatible projection definition.
74
+ */
75
+ export type ProjectionDefinition<TName extends string = string> = {
76
+ name: TName;
77
+ canHandle: string[];
78
+ handle: (events: { type: string }[]) => Promise<void>;
79
+ };
80
+
81
+ /**
82
+ * Database builder with projection support
83
+ */
84
+ export type DbBuilder<TRelations extends AnyRelations, TRepos extends Record<string, unknown>> = {
85
+ readonly db: NodePgDatabase<TRelations>;
86
+ readonly isDev: boolean;
87
+
88
+ registerRepository<K extends string, R>(
89
+ key: K,
90
+ factory: (db: NodePgDatabase<TRelations>) => R,
91
+ ): DbBuilder<TRelations, TRepos & Record<K, R>>;
92
+
93
+ projectionMiddleware(fn: ProjectionMiddleware): ProjectionMiddleware;
94
+
95
+ readonly projection: ProjectionBuilderRoot<TRepos, TRelations>;
96
+ };
97
+
98
+ /**
99
+ * Projection builder root that can have middleware applied
100
+ */
101
+ export type ProjectionBuilderRoot<TRepos, TRelations extends AnyRelations = AnyRelations> = {
102
+ use(middleware: ProjectionMiddleware): ProjectionBuilderRoot<TRepos, TRelations>;
103
+ } & ProjectionBuilder<never, TRepos, TRelations>;
104
+
105
+ /**
106
+ * Schema configuration step
107
+ */
108
+ export type SchemaBuilder<TRelations extends AnyRelations> = {
109
+ create(options: {
110
+ connectionString: string;
111
+ isDev?: boolean;
112
+ poolConfig?: Partial<PoolConfig>;
113
+ }): DbBuilder<TRelations, Record<string, never>>;
114
+ };
115
+
116
+ /**
117
+ * Initial builder step
118
+ */
119
+ export type InitialBuilder = {
120
+ /**
121
+ * Set the database tables. Internally builds a relations config via
122
+ * `defineRelations()` so consumers keep passing a plain table record.
123
+ *
124
+ * Use this for backends that don't define explicit relations and rely on
125
+ * auto-derivation (no joins). For explicit RQBv2 relations, prefer
126
+ * `.relations(...)` with a `defineRelations(tables, cb)` result.
127
+ */
128
+ schema<TSchema extends Record<string, unknown>>(
129
+ schema: TSchema,
130
+ ): SchemaBuilder<ReturnType<typeof defineRelations<TSchema>>>;
131
+
132
+ /**
133
+ * Pass a pre-built relations object (the result of
134
+ * `defineRelations(tables, cb)`) so the database knows about every table
135
+ * AND the explicit one/many relations declared by the application.
136
+ */
137
+ relations<TRelations extends AnyRelations>(relations: TRelations): SchemaBuilder<TRelations>;
138
+ };
139
+
140
+ /**
141
+ * Applies a middleware chain around a core handler function.
142
+ */
143
+ function applyMiddleware(
144
+ middleware: ProjectionMiddleware[],
145
+ name: string,
146
+ eventCount: number,
147
+ coreHandler: () => Promise<void>,
148
+ ): Promise<void> {
149
+ const meta = { name, eventCount };
150
+
151
+ let next = coreHandler;
152
+ for (let i = middleware.length - 1; i >= 0; i--) {
153
+ const mw = middleware[i]!;
154
+ const currentNext = next;
155
+ next = () => mw({ next: currentNext, meta });
156
+ }
157
+
158
+ return next();
159
+ }
160
+
161
+ class ProjectionBuilderImpl<
162
+ TEvent,
163
+ TRepos,
164
+ TRelations extends AnyRelations,
165
+ TName extends string = string,
166
+ >
167
+ implements
168
+ ProjectionBuilder<TEvent, TRepos, TRelations>,
169
+ ProjectionBuilderWithName<TEvent, TRepos, TRelations, TName>,
170
+ ProjectionBuilderWithEvents<TEvent, TRepos, TRelations, TName>,
171
+ ProjectionBuilderRoot<TRepos, TRelations>
172
+ {
173
+ private _name?: string;
174
+ private _eventTypes?: string[];
175
+ private _middleware: ProjectionMiddleware[] = [];
176
+
177
+ constructor(
178
+ private readonly repos: TRepos,
179
+ private readonly _db: NodePgDatabase<TRelations>,
180
+ middleware?: ProjectionMiddleware[],
181
+ ) {
182
+ if (middleware) {
183
+ this._middleware = [...middleware];
184
+ }
185
+ }
186
+
187
+ named<N extends string>(name: N): ProjectionBuilderWithName<TEvent, TRepos, TRelations, N> {
188
+ debugProjection('[builder] Setting projection name: %s', name);
189
+ this._name = name;
190
+ return this as unknown as ProjectionBuilderImpl<TEvent, TRepos, TRelations, N>;
191
+ }
192
+
193
+ on<TEventType extends { type: string }>(
194
+ eventTypes: readonly TEventType['type'][] | TEventType['type'][],
195
+ ): ProjectionBuilderWithEvents<TEventType, TRepos, TRelations, TName> {
196
+ debugProjection('[builder] Registering event types: %o', eventTypes);
197
+ this._eventTypes = [...eventTypes];
198
+ return this as unknown as ProjectionBuilderImpl<TEventType, TRepos, TRelations, TName>;
199
+ }
200
+
201
+ handle(handler: ProjectionHandler<TEvent, TRepos, TRelations>): ProjectionDefinition<TName> {
202
+ if (!this._name) {
203
+ debugProjection('[builder] Error: Projection name is required');
204
+ throw new Error('Projection name is required. Call .named() first.');
205
+ }
206
+ if (!this._eventTypes) {
207
+ debugProjection('[builder] Error: Event types are required');
208
+ throw new Error('Event types are required. Call .on() first.');
209
+ }
210
+
211
+ const name = this._name as TName;
212
+ const eventTypes = this._eventTypes;
213
+ const middleware = this._middleware;
214
+ const repos = this.repos;
215
+ const db = this._db;
216
+
217
+ debugProjection('[builder] Creating projection definition (name: %s, eventTypes: %o, middlewareCount: %d)',
218
+ name, eventTypes, middleware.length);
219
+
220
+ return {
221
+ name,
222
+ canHandle: eventTypes,
223
+ handle: async (events: { type: string }[]) => {
224
+ const context: ProjectionHandlerContext<TRepos, TRelations> = { repo: repos, db };
225
+ const coreHandler = () => handler(events as TEvent[], context);
226
+
227
+ if (middleware.length > 0) {
228
+ await applyMiddleware(middleware, name, events.length, coreHandler);
229
+ } else {
230
+ await coreHandler();
231
+ }
232
+ },
233
+ };
234
+ }
235
+
236
+ use(middleware: ProjectionMiddleware): ProjectionBuilderRoot<TRepos, TRelations> {
237
+ debugProjection('[builder] Adding projection middleware (currentCount: %d)', this._middleware.length);
238
+ const newBuilder = new ProjectionBuilderImpl<TEvent, TRepos, TRelations, TName>(this.repos, this._db, [
239
+ ...this._middleware,
240
+ middleware,
241
+ ]);
242
+ debugProjection('[builder] Middleware added (newCount: %d)', this._middleware.length + 1);
243
+ return newBuilder;
244
+ }
245
+ }
246
+
247
+ class DbBuilderImpl<TRelations extends AnyRelations, TRepos extends Record<string, unknown>>
248
+ implements DbBuilder<TRelations, TRepos>
249
+ {
250
+ readonly db: NodePgDatabase<TRelations>;
251
+ readonly isDev: boolean;
252
+ private _repos?: TRepos;
253
+ private readonly _repoFactories: Map<string, (db: NodePgDatabase<TRelations>) => unknown>;
254
+
255
+ constructor(
256
+ db: NodePgDatabase<TRelations>,
257
+ isDev: boolean,
258
+ repoFactories?: Map<string, (db: NodePgDatabase<TRelations>) => unknown>,
259
+ ) {
260
+ this.db = db;
261
+ this.isDev = isDev;
262
+ this._repoFactories = repoFactories ?? new Map();
263
+ }
264
+
265
+ registerRepository<K extends string, R>(
266
+ key: K,
267
+ factory: (db: NodePgDatabase<TRelations>) => R,
268
+ ): DbBuilder<TRelations, TRepos & Record<K, R>> {
269
+ debugProjection('[builder] Registering repository (key: %s, currentCount: %d)', key, this._repoFactories.size);
270
+ const newFactories = new Map(this._repoFactories);
271
+ newFactories.set(key, factory);
272
+ debugProjection('[builder] Repository registered (key: %s, newCount: %d)', key, newFactories.size);
273
+ return new DbBuilderImpl<TRelations, TRepos & Record<K, R>>(this.db, this.isDev, newFactories) as DbBuilder<
274
+ TRelations,
275
+ TRepos & Record<K, R>
276
+ >;
277
+ }
278
+
279
+ projectionMiddleware(fn: ProjectionMiddleware): ProjectionMiddleware {
280
+ return fn;
281
+ }
282
+
283
+ get projection(): ProjectionBuilderRoot<TRepos, TRelations> {
284
+ if (!this._repos) {
285
+ debugProjection('[builder] Initializing repositories from factories (count: %d)', this._repoFactories.size);
286
+ const repos: Record<string, unknown> = {};
287
+ for (const [key, factory] of this._repoFactories) {
288
+ debugProjection('[builder] Creating repository: %s', key);
289
+ repos[key] = factory(this.db);
290
+ }
291
+ this._repos = repos as TRepos;
292
+ debugProjection('[builder] All repositories initialized');
293
+ }
294
+ debugProjection('[builder] Creating projection builder');
295
+ return new ProjectionBuilderImpl<never, TRepos, TRelations>(this._repos, this.db);
296
+ }
297
+ }
298
+
299
+ class SchemaBuilderImpl<TRelations extends AnyRelations> implements SchemaBuilder<TRelations> {
300
+ constructor(private readonly relations: TRelations) {}
301
+
302
+ create(options: {
303
+ connectionString: string;
304
+ isDev?: boolean;
305
+ poolConfig?: Partial<PoolConfig>;
306
+ }): DbBuilder<TRelations, Record<string, never>> {
307
+ debugProjection('[builder] Creating database builder (isDev: %s, hasPoolConfig: %s)',
308
+ options.isDev ?? false, !!options.poolConfig);
309
+
310
+ const poolConfig: PoolConfig = {
311
+ connectionString: options.connectionString,
312
+ ...options.poolConfig,
313
+ };
314
+
315
+ debugProjection('[builder] Initializing database with pool config');
316
+ const db = initDbWithOptions<TRelations>(poolConfig, {
317
+ relations: this.relations,
318
+ logger: options.isDev,
319
+ });
320
+
321
+ debugProjection('[builder] Database builder created successfully');
322
+ return new DbBuilderImpl<TRelations, Record<string, never>>(db, options.isDev ?? false);
323
+ }
324
+ }
325
+
326
+ class InitialBuilderImpl implements InitialBuilder {
327
+ schema<TSchema extends Record<string, unknown>>(
328
+ schema: TSchema,
329
+ ): SchemaBuilder<ReturnType<typeof defineRelations<TSchema>>> {
330
+ debugProjection('[builder] Setting database schema (deriving relations via defineRelations)');
331
+ const relations = defineRelations(schema) as ReturnType<typeof defineRelations<TSchema>>;
332
+ return new SchemaBuilderImpl(relations);
333
+ }
334
+
335
+ relations<TRelations extends AnyRelations>(relations: TRelations): SchemaBuilder<TRelations> {
336
+ debugProjection('[builder] Setting database relations (explicit RQBv2)');
337
+ return new SchemaBuilderImpl(relations);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Initialize the database builder.
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const d = initDb
347
+ * .schema(schema)
348
+ * .create({
349
+ * connectionString: env.DB_URL,
350
+ * isDev: process.env.NODE_ENV === 'development',
351
+ * });
352
+ *
353
+ * const db = d.db;
354
+ * const projection = d.projection.named('my-proj').on(['EventA']).handle(async (events, ctx) => {
355
+ * // handler
356
+ * });
357
+ * ```
358
+ */
359
+ export const initDb: InitialBuilder = new InitialBuilderImpl();
package/src/bun.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { SQL } from 'bun';
2
+ import type { AnyRelations } from 'drizzle-orm';
3
+ import type { BunSQLDatabase } from 'drizzle-orm/bun-sql/postgres';
4
+ import { drizzle } from 'drizzle-orm/bun-sql/postgres';
5
+ import type { DrizzlePgConfig } from 'drizzle-orm/pg-core/utils';
6
+
7
+ import { debugInit } from './debug.js';
8
+ import { env } from './env.js';
9
+
10
+ /**
11
+ * Initialize database with Bun's native SQL driver.
12
+ * @param connectionString - PostgreSQL connection string.
13
+ * @param config - Drizzle pg configuration.
14
+ */
15
+ export const initDb = <TRelations extends AnyRelations>(
16
+ connectionString: string,
17
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
18
+ ): BunSQLDatabase<TRelations> => {
19
+ debugInit('[bun] Initializing database with connection string (logger: %s)', !!config.logger);
20
+
21
+ const client = new SQL(connectionString);
22
+
23
+ const db = drizzle<TRelations>({ ...config, client });
24
+
25
+ debugInit('[bun] Database initialized successfully with Bun SQL driver');
26
+
27
+ return db;
28
+ };
29
+
30
+ /**
31
+ * Create a database connection using environment configuration with Bun SQL.
32
+ * @param config - Drizzle pg configuration.
33
+ * @throws Error if DB_URL is not configured.
34
+ */
35
+ export function createDb<TRelations extends AnyRelations>(
36
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
37
+ ): BunSQLDatabase<TRelations> {
38
+ debugInit('[bun] Creating database from environment configuration');
39
+
40
+ const connectionUrl = env.DB_URL;
41
+ if (!connectionUrl) {
42
+ debugInit('[bun] Database creation failed: DB_URL not configured');
43
+ throw new Error('Database URL is not configured. Please set DB_URL environment variable.');
44
+ }
45
+
46
+ const db = initDb(connectionUrl, {
47
+ ...config,
48
+ logger: config.logger ?? (env.DB_LOGGING || env.NODE_ENV === 'development'),
49
+ });
50
+
51
+ debugInit('[bun] Database created successfully');
52
+
53
+ return db;
54
+ }
55
+
56
+ export { getDbConnectionOptions, getDbConnectionParams } from './connection-options.js';
57
+
58
+ export type { BunSQLDatabase } from 'drizzle-orm/bun-sql/postgres';
@@ -0,0 +1,5 @@
1
+ # @arki/db/client
2
+
3
+ Reserved client-side surface of `@arki/db`. V1 contains only type-level re-exports
4
+ (zero runtime cost). Future Drizzle-on-client / Tier-3 sync work (Electric / PowerSync /
5
+ Zero adapters) will land here. See [packages/offline/docs/2026-05-16-arki-offline-design.md](../../../offline/docs/2026-05-16-arki-offline-design.md).
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Branded ID type re-exports for the client surface.
3
+ *
4
+ * V1: @arki/contracts does not yet export named branded ID types.
5
+ * Future versions will re-export them here once they are defined, e.g.:
6
+ *
7
+ * export type { UserId, OrgId, AssetId } from '@arki/contracts';
8
+ *
9
+ * See packages/offline/docs/2026-05-16-arki-offline-design.md §3.3.
10
+ */
11
+ export type {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @arki/db/client — types-only client surface.
3
+ *
4
+ * V1 is intentionally empty: this subpath claims the namespace for future
5
+ * Drizzle-on-client / Tier-3 sync work (Electric / PowerSync / Zero adapters).
6
+ * No runtime imports are allowed here — this file must remain free of any
7
+ * server-only dependencies (postgres, drizzle-orm connection utilities, etc.).
8
+ *
9
+ * See packages/offline/docs/2026-05-16-arki-offline-design.md §3.3.
10
+ */
11
+ export type * from './ids.js';
@@ -0,0 +1,49 @@
1
+ import { debugFactory } from './debug.js';
2
+ import { env } from './env.js';
3
+
4
+ /**
5
+ * Get database connection options from environment variables
6
+ * @returns Connection options object for pg Pool
7
+ */
8
+ export function getDbConnectionOptions() {
9
+ debugFactory('[factory] Retrieving database connection options from environment');
10
+
11
+ const connectionUrl = env.DB_URL;
12
+ const options = {
13
+ connectionString: connectionUrl,
14
+ min: env.DB_POOL_MIN,
15
+ max: env.DB_POOL_MAX,
16
+ idleTimeoutMillis: env.DB_POOL_IDLE_TIMEOUT,
17
+ connectionTimeoutMillis: env.DB_POOL_CONNECTION_TIMEOUT,
18
+ statement_timeout: env.DB_STATEMENT_TIMEOUT,
19
+ query_timeout: env.DB_QUERY_TIMEOUT,
20
+ idle_in_transaction_session_timeout: env.DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
21
+ };
22
+
23
+ debugFactory('[factory] Connection options retrieved (hasUrl: %s)', !!connectionUrl);
24
+
25
+ return options;
26
+ }
27
+
28
+ /**
29
+ * Get individual database connection parameters from environment variables
30
+ * Useful when DB_URL is not available but individual parameters are set
31
+ * @returns Connection parameters object
32
+ */
33
+ export function getDbConnectionParams() {
34
+ debugFactory('[factory] Retrieving database connection parameters from environment');
35
+
36
+ const params = {
37
+ user: env.PGUSER,
38
+ password: env.PGPASSWORD,
39
+ host: env.PGHOST,
40
+ port: env.PGPORT,
41
+ database: env.PGDATABASE,
42
+ ssl: env.PGSSLMODE === 'disable' ? false : { rejectUnauthorized: env.PGSSLMODE === 'verify-full' },
43
+ };
44
+
45
+ debugFactory('[factory] Connection parameters retrieved (host: %s, port: %d, database: %s, ssl: %s)',
46
+ params.host, params.port, params.database, env.PGSSLMODE);
47
+
48
+ return params;
49
+ }
package/src/debug.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { createDebugLogger } from '@arki/log/debug';
2
+
3
+ /**
4
+ * Debug logger for database initialization and connection operations
5
+ * Enable with: DEBUG=db:init
6
+ */
7
+ export const debugInit = createDebugLogger('db:init');
8
+
9
+ /**
10
+ * Debug logger for database factory operations
11
+ * Enable with: DEBUG=db:factory
12
+ */
13
+ export const debugFactory = createDebugLogger('db:factory');
14
+
15
+ /**
16
+ * Debug logger for projection builder operations
17
+ * Enable with: DEBUG=db:projection
18
+ */
19
+ export const debugProjection = createDebugLogger('db:projection');
20
+
21
+ /**
22
+ * Enable all database debug logs
23
+ * Enable with: DEBUG=db:*
24
+ */
package/src/dot.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * DOT adapter for `@arki/db`.
3
+ *
4
+ * Wraps the Drizzle database initialization as a DOT pip. The pip
5
+ * opens a database connection in `boot`, publishes the Drizzle handle as
6
+ * `services.db`, and tears down the underlying client in `dispose`
7
+ * (reverse declaration order).
8
+ *
9
+ * Two drivers are supported:
10
+ * - `'pg'` (default) — `node-postgres` pool. Reads `DB_URL` and the
11
+ * `DB_POOL_*` env vars via `@arki/db`'s built-in env loader.
12
+ * - `'pglite'` — embedded PGlite. Pass `dataDir`, or `memory: true` for
13
+ * an in-memory instance. Use for tests, single-binary apps, or local
14
+ * development without a real Postgres server.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { defineApp } from '@arki/dot';
19
+ * import { db } from '@arki/db/dot';
20
+ * import { defineRelations } from '@arki/db/orm';
21
+ * import { schema } from './schema';
22
+ *
23
+ * const relations = defineRelations(schema, (b) => ({ ... }));
24
+ *
25
+ * // Node-postgres (default), reads DB_URL from env:
26
+ * const app = await defineApp('my-app')
27
+ * .use(db({ relations }))
28
+ * .boot();
29
+ *
30
+ * // PGlite in-memory (great for tests):
31
+ * const testApp = await defineApp('test')
32
+ * .use(db({ driver: 'pglite', memory: true, relations }))
33
+ * .boot();
34
+ * ```
35
+ *
36
+ * To mount a second database scope (e.g. primary + reporting) in the same
37
+ * app, rename the published wire key at the mount site:
38
+ *
39
+ * ```ts
40
+ * import { rename } from '@arki/dot';
41
+ *
42
+ * .use(db({ relations }))
43
+ * .use(rename(db({ driver: 'pglite', memory: true, relations }), { db: 'reportsDb' }, 'reports-db'))
44
+ * ```
45
+ *
46
+ * The `@arki/dot` package is an OPTIONAL peer of `@arki/db`. Importing
47
+ * this adapter without `@arki/dot` installed will fail at module load —
48
+ * that is intentional: the adapter only makes sense in a DOT app.
49
+ */
50
+
51
+ import type { AnyRelations, Logger } from 'drizzle-orm';
52
+ import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
53
+ import type { PgliteDatabase } from 'drizzle-orm/pglite';
54
+
55
+ import type { EmptyShape, Pip } from '@arki/dot/pip';
56
+ import { pip, DotPipError } from '@arki/dot/pip';
57
+
58
+ import type { PgliteInitOptions } from './runtime-local.js';
59
+ import { env } from './env.js';
60
+ import { createDb } from './factory.js';
61
+ import { initDbRuntimeLocal } from './runtime-local.js';
62
+
63
+ /**
64
+ * Stable error codes thrown by the db pip. Exported so consumers and
65
+ * coding agents can match against them — never parse the message.
66
+ *
67
+ * @see packages/dot/docs/principles.md — principle 1.3 ("errors are part
68
+ * of the API") and principle 4 ("agent-discoverable everywhere").
69
+ */
70
+ export const DB_PIP_ERROR_CODES = {
71
+ /** boot was called without a configured DB_URL (pg driver). */
72
+ dbUrlNotConfigured: 'DB_PIP_E001',
73
+ } as const;
74
+
75
+ /**
76
+ * Common options shared by all DB driver variants.
77
+ */
78
+ type BaseDbDotOptions<TRelations extends AnyRelations> = {
79
+ /** Drizzle 1.0 relations config (build via `defineRelations`). */
80
+ readonly relations: TRelations;
81
+ /** Drizzle logger override — `true`/`false` or a custom `Logger` instance. */
82
+ readonly logger?: boolean | Logger;
83
+ };
84
+
85
+ /** Options for the node-postgres driver (the default). */
86
+ export type PgDbDotOptions<TRelations extends AnyRelations> = BaseDbDotOptions<TRelations> & {
87
+ /** Driver discriminant. Defaults to `'pg'` when omitted. */
88
+ readonly driver?: 'pg';
89
+ };
90
+
91
+ /** Options for the embedded-PGlite driver. */
92
+ export type PgliteDbDotOptions<TRelations extends AnyRelations> = BaseDbDotOptions<TRelations> &
93
+ PgliteInitOptions & {
94
+ /** Driver discriminant. */
95
+ readonly driver: 'pglite';
96
+ };
97
+
98
+ /** Discriminated union of all supported driver option shapes. */
99
+ export type DbDotOptions<TRelations extends AnyRelations> = PgDbDotOptions<TRelations> | PgliteDbDotOptions<TRelations>;
100
+
101
+ /**
102
+ * Services published by the db adapter. Keyed by the driver — for the
103
+ * default `'pg'` driver, `services.db` is a `NodePgDatabase<TRelations>`;
104
+ * for `'pglite'` it is a `PgliteDatabase<TRelations>`.
105
+ */
106
+ export type DbServices<TRelations extends AnyRelations, TDriver extends 'pg' | 'pglite' = 'pg'> = {
107
+ readonly db: TDriver extends 'pglite' ? PgliteDatabase<TRelations> : NodePgDatabase<TRelations>;
108
+ };
109
+
110
+ /**
111
+ * Build a DOT pip that opens a Drizzle database and publishes it as
112
+ * a service. The kernel calls `dispose` in reverse declaration order to
113
+ * close the underlying pool / PGlite instance.
114
+ */
115
+ export function db<TRelations extends AnyRelations>(
116
+ options: PgDbDotOptions<TRelations>,
117
+ ): Pip<EmptyShape, DbServices<TRelations, 'pg'>>;
118
+ export function db<TRelations extends AnyRelations>(
119
+ options: PgliteDbDotOptions<TRelations>,
120
+ ): Pip<EmptyShape, DbServices<TRelations, 'pglite'>>;
121
+ export function db<TRelations extends AnyRelations>(
122
+ options: DbDotOptions<TRelations>,
123
+ ): Pip<EmptyShape, DbServices<TRelations, 'pg' | 'pglite'>> {
124
+ const driver = options.driver ?? 'pg';
125
+
126
+ if (driver === 'pglite') {
127
+ const pgliteOptions = options as PgliteDbDotOptions<TRelations>;
128
+ // PGlite path: open the embedded instance and Drizzle handle in boot.
129
+ // We capture the raw PGlite client at boot-time so `dispose` can close
130
+ // it deterministically. `services.db` is the Drizzle handle only —
131
+ // exposing the PGlite client would leak driver-specific surface.
132
+ let pgliteHandle: { close: () => Promise<void> } | undefined;
133
+ return pip({
134
+ name: 'db',
135
+ version: '0.1.0',
136
+ configure(ctx) {
137
+ ctx.registerService('db', 'db');
138
+ },
139
+ async boot(): Promise<DbServices<TRelations, 'pglite'>> {
140
+ const { db: handle, pglite } = await initDbRuntimeLocal<TRelations>(
141
+ {
142
+ ...(pgliteOptions.dataDir === undefined ? {} : { dataDir: pgliteOptions.dataDir }),
143
+ ...(pgliteOptions.memory === undefined ? {} : { memory: pgliteOptions.memory }),
144
+ ...(pgliteOptions.extensions === undefined ? {} : { extensions: pgliteOptions.extensions }),
145
+ },
146
+ { relations: pgliteOptions.relations },
147
+ );
148
+ pgliteHandle = pglite;
149
+ return { db: handle };
150
+ },
151
+ async dispose() {
152
+ if (pgliteHandle !== undefined) {
153
+ await pgliteHandle.close();
154
+ pgliteHandle = undefined;
155
+ }
156
+ },
157
+ }) as Pip<EmptyShape, DbServices<TRelations, 'pg' | 'pglite'>>;
158
+ }
159
+
160
+ // Node-postgres path: createDb already reads env vars and constructs the
161
+ // pool. Drizzle owns the pool — closing it goes via the `$client.end()`
162
+ // path that node-postgres exposes through Drizzle's handle.
163
+ return pip({
164
+ name: 'db',
165
+ version: '0.1.0',
166
+ configure(ctx) {
167
+ ctx.registerService('db', 'db');
168
+ },
169
+ boot(): DbServices<TRelations, 'pg'> {
170
+ // Validate at the pip boundary so the DOT lifecycle gets a coded
171
+ // error. `createDb` still throws raw `Error` for non-DOT consumers
172
+ // (its public contract is unchanged); the check here makes sure we
173
+ // never reach it without a URL.
174
+ if (env.DB_URL === undefined || env.DB_URL === '') {
175
+ throw new DotPipError({
176
+ code: DB_PIP_ERROR_CODES.dbUrlNotConfigured,
177
+ message: '[db] DB_URL is not configured.',
178
+ remediation:
179
+ 'Set DB_URL in the environment before booting the app, or switch the pip to the pglite driver for in-process databases.',
180
+ docsUrl: 'https://arki.dev/dot/errors/db-pip-e001',
181
+ });
182
+ }
183
+ const handle = createDb<TRelations>({
184
+ relations: options.relations,
185
+ ...(options.logger === undefined ? {} : { logger: options.logger }),
186
+ });
187
+ return { db: handle };
188
+ },
189
+ async dispose({ db: handle }) {
190
+ // Drizzle exposes the underlying pg.Pool as `$client`. We call its
191
+ // `end()` to drain in-flight queries and close all sockets.
192
+ const pgHandle = handle as NodePgDatabase<TRelations> & {
193
+ $client?: { end: () => Promise<void> };
194
+ };
195
+ if (pgHandle.$client !== undefined) {
196
+ await pgHandle.$client.end();
197
+ }
198
+ },
199
+ }) as Pip<EmptyShape, DbServices<TRelations, 'pg' | 'pglite'>>;
200
+ }
package/src/env.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { defineEnv } from '@arki/env/core';
2
+
3
+ import { z } from '@arki/contracts';
4
+
5
+ export const env = defineEnv({
6
+ name: '@arki/db',
7
+ /**
8
+ * Environment variables schema for database services (PostgreSQL)
9
+ */
10
+ server: {
11
+ // General Configuration
12
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
13
+
14
+ // Database Connection - Primary method (connection string)
15
+ // DB_URL is the canonical @arki/db name. DATABASE_URL (PG/Coolify/12-factor
16
+ // standard) is also accepted as a fallback via the runtimeEnv alias below,
17
+ // which resolves to this same `env.DB_URL` field so consumers don't change.
18
+ DB_URL: z.url(),
19
+
20
+ // Database Connection - Individual parameters (alternative to DB_URL)
21
+ PGUSER: z.string().optional(),
22
+ PGPASSWORD: z.string().optional(),
23
+ PGHOST: z.string().default('localhost'),
24
+ PGPORT: z.coerce.number().default(5432),
25
+ PGDATABASE: z.string().optional(),
26
+ PGSSLMODE: z.enum(['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']).default('prefer'),
27
+
28
+ // Connection Pool Configuration
29
+ DB_POOL_MIN: z.coerce.number().default(2),
30
+ DB_POOL_MAX: z.coerce.number().default(20),
31
+ DB_POOL_IDLE_TIMEOUT: z.coerce.number().default(30_000), // 30 seconds
32
+ DB_POOL_CONNECTION_TIMEOUT: z.coerce.number().default(2000), // 2 seconds
33
+
34
+ // Database Behavior Configuration
35
+ DB_LOGGING: z.coerce.boolean().default(false),
36
+ DB_MIGRATIONS_FOLDER: z.string().default('./migrations'),
37
+ DB_SCHEMA: z.string().default('public'),
38
+
39
+ // Performance and Reliability
40
+ DB_STATEMENT_TIMEOUT: z.coerce.number().default(30_000), // 30 seconds
41
+ DB_QUERY_TIMEOUT: z.coerce.number().default(60_000), // 1 minute
42
+ DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: z.coerce.number().default(10_000), // 10 seconds
43
+
44
+ // Development and Testing
45
+ DB_SEED_ON_START: z
46
+ .string()
47
+ .optional()
48
+ .transform(val => val === 'true'),
49
+ DB_DROP_ON_START: z
50
+ .string()
51
+ .optional()
52
+ .transform(val => val === 'true'),
53
+ },
54
+
55
+ client: {},
56
+ clientPrefix: '',
57
+
58
+ /**
59
+ * Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
60
+ */
61
+ runtimeEnv: {
62
+ NODE_ENV: process.env['NODE_ENV'],
63
+ // Resolve from `DB_URL` (canonical) first, falling back to `DATABASE_URL`
64
+ // (PG/Coolify/12-factor standard) so deployments using the standard name
65
+ // still work. Closes the silent divergence between app schemas keyed on
66
+ // DATABASE_URL and this package's DB_URL schema.
67
+ DB_URL: process.env['DB_URL'] ?? process.env['DATABASE_URL'],
68
+ PGUSER: process.env['PGUSER'],
69
+ PGPASSWORD: process.env['PGPASSWORD'],
70
+ PGHOST: process.env['PGHOST'],
71
+ PGPORT: process.env['PGPORT'],
72
+ PGDATABASE: process.env['PGDATABASE'],
73
+ PGSSLMODE: process.env['PGSSLMODE'],
74
+ DB_POOL_MIN: process.env['DB_POOL_MIN'],
75
+ DB_POOL_MAX: process.env['DB_POOL_MAX'],
76
+ DB_POOL_IDLE_TIMEOUT: process.env['DB_POOL_IDLE_TIMEOUT'],
77
+ DB_POOL_CONNECTION_TIMEOUT: process.env['DB_POOL_CONNECTION_TIMEOUT'],
78
+ DB_LOGGING: process.env['DB_LOGGING'],
79
+ DB_MIGRATIONS_FOLDER: process.env['DB_MIGRATIONS_FOLDER'],
80
+ DB_SCHEMA: process.env['DB_SCHEMA'],
81
+ DB_STATEMENT_TIMEOUT: process.env['DB_STATEMENT_TIMEOUT'],
82
+ DB_QUERY_TIMEOUT: process.env['DB_QUERY_TIMEOUT'],
83
+ DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: process.env['DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT'],
84
+ DB_SEED_ON_START: process.env['DB_SEED_ON_START'],
85
+ DB_DROP_ON_START: process.env['DB_DROP_ON_START'],
86
+ },
87
+
88
+ options: {
89
+ skipValidation:
90
+ !!process.env['SKIP_ENV_VALIDATION'] || !!process.env['CI'] || process.env['NODE_ENV'] === 'test',
91
+ },
92
+ });
package/src/factory.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { AnyRelations, Logger } from 'drizzle-orm';
2
+
3
+ import { getDbConnectionOptions } from './connection-options.js';
4
+ import { debugFactory } from './debug.js';
5
+ import { env } from './env.js';
6
+ import { initDbWithOptions } from './init.js';
7
+
8
+ /**
9
+ * Create a database connection using environment configuration.
10
+ *
11
+ * `relations` is required (mark the field even when empty) so TypeScript can
12
+ * infer the precise relations type for `db.query.*` autocomplete. Use
13
+ * `defineRelations(schema, callback)` to build the value.
14
+ *
15
+ * @throws Error if DB_URL is not configured.
16
+ */
17
+ export function createDb<TRelations extends AnyRelations>(
18
+ config: {
19
+ relations: TRelations;
20
+ logger?: boolean | Logger | undefined;
21
+ },
22
+ ) {
23
+ debugFactory('[factory] Creating database from environment configuration');
24
+
25
+ const connectionUrl = env.DB_URL;
26
+ if (!connectionUrl) {
27
+ debugFactory('[factory] Database creation failed: DB_URL not configured');
28
+ throw new Error('Database URL is not configured. Please set DB_URL environment variable.');
29
+ }
30
+
31
+ const connectionOptions = getDbConnectionOptions();
32
+
33
+ debugFactory('[factory] Connection options (min: %d, max: %d, idleTimeout: %dms, connTimeout: %dms)',
34
+ connectionOptions.min, connectionOptions.max,
35
+ connectionOptions.idleTimeoutMillis, connectionOptions.connectionTimeoutMillis);
36
+
37
+ const db = initDbWithOptions<TRelations>(connectionOptions, {
38
+ ...config,
39
+ logger: config.logger ?? (env.DB_LOGGING || env.NODE_ENV === 'development'),
40
+ });
41
+
42
+ debugFactory('[factory] Database created successfully');
43
+
44
+ return db;
45
+ }
46
+
47
+ export { getDbConnectionOptions, getDbConnectionParams } from './connection-options.js';
@@ -0,0 +1,172 @@
1
+ import { createId as createCuid } from '@paralleldrive/cuid2';
2
+ import type { PgColumn } from 'drizzle-orm/pg-core';
3
+ import { varchar } from 'drizzle-orm/pg-core';
4
+
5
+ import { COLUMN_ZOD_SCHEMA, z } from '@arki/contracts';
6
+
7
+ // Core ID creation
8
+ export const createId = () => createCuid();
9
+
10
+ export const createPrefixedId = <T extends string>(prefix: T): `${T}${string}` => {
11
+ return `${prefix}${createId()}`;
12
+ };
13
+
14
+ /**
15
+ * Stash a Zod schema on a Drizzle column builder so that
16
+ * `@arki/contracts`'s `createSelectSchema`/`createInsertSchema`/`createUpdateSchema`
17
+ * wrappers auto-apply it as a refine — turning the brand from a phantom-only
18
+ * type into a real runtime check.
19
+ *
20
+ * The builder's `config` object is the SAME reference passed to the constructed
21
+ * column (see `PgVarcharBuilder.build` → `new PgVarchar(table, this.config)`),
22
+ * so writing here is readable from the column later.
23
+ */
24
+ function tagColumnSchema<TBuilder>(builder: TBuilder, schema: z.ZodTypeAny): TBuilder {
25
+ const cfg = (builder as unknown as { config: Record<string | symbol, unknown> }).config;
26
+ cfg[COLUMN_ZOD_SCHEMA] = schema;
27
+ return builder;
28
+ }
29
+
30
+ /**
31
+ * Type-safe ID factory that generates all ID-related utilities at once
32
+ *
33
+ * @param prefix - The prefix for the ID type (e.g., 'pt', 'usr', 'ant')
34
+ * @returns An object with create function, schema, and column helpers
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * export type PoetId = `pt${string}`;
39
+ * const poetId = createIdFactory<'pt', PoetId>('pt');
40
+ *
41
+ * export const createPoetId = poetId.create;
42
+ * export const poetIdSchema = poetId.schema;
43
+ *
44
+ * export const poets = schema.table('poets', {
45
+ * id: poetId.primaryColumn(),
46
+ * // ... other columns
47
+ * });
48
+ * ```
49
+ */
50
+ export function createIdFactory<TPrefix extends string, TId extends `${TPrefix}${string}`>(prefix: TPrefix) {
51
+ /**
52
+ * Zod validation schema for the ID type. Uses refine instead of transform
53
+ * for better type safety; auto-applied at parse time by @arki/contracts'
54
+ * schema wrappers when this factory's columns appear on the table.
55
+ */
56
+ const schema = z
57
+ .string()
58
+ .startsWith(prefix)
59
+ .brand<TId>()
60
+ .transform(id => id as TId);
61
+
62
+ const factory = {
63
+ /**
64
+ * Creates a new ID with the specified prefix
65
+ */
66
+ create: (): TId => createPrefixedId(prefix) as TId,
67
+ /**
68
+ * Creates a new ID with the specified prefix
69
+ */
70
+ make: (): TId => createPrefixedId(prefix) as TId,
71
+ /**
72
+ * Creates a new ID with the specified prefix
73
+ */
74
+ new: (): TId => createPrefixedId(prefix) as TId,
75
+
76
+ schema,
77
+
78
+ /**
79
+ * Drizzle column helper for primary key columns
80
+ * Automatically configures as varchar(256), not null, primary key, with proper type and default
81
+ */
82
+ primaryColumn: (name = 'id') =>
83
+ tagColumnSchema(
84
+ varchar(name, { length: 256 })
85
+ .notNull()
86
+ .primaryKey()
87
+ .$type<TId>()
88
+ .$defaultFn(() => createPrefixedId(prefix) as TId),
89
+ schema,
90
+ ),
91
+
92
+ optionalColumn: (name: string) =>
93
+ tagColumnSchema(
94
+ varchar(name, { length: 256 })
95
+ .$type<TId | null>()
96
+ .$defaultFn(() => null),
97
+ schema,
98
+ ),
99
+
100
+ requiredColumn: (name: string) =>
101
+ tagColumnSchema(
102
+ varchar(name, { length: 256 })
103
+ .notNull()
104
+ .$type<TId>()
105
+ .$defaultFn(() => createPrefixedId(prefix) as TId),
106
+ schema,
107
+ ),
108
+
109
+ /**
110
+ * Drizzle column helper for foreign key reference columns
111
+ * Configures as varchar(256), not null, with proper type (no default or primary key)
112
+ */
113
+ reference: (name: string) =>
114
+ tagColumnSchema(varchar(name, { length: 256 }).notNull().$type<TId>(), schema),
115
+
116
+ /**
117
+ * Drizzle column helper for optional foreign key reference columns
118
+ * Configures as varchar(256), nullable, with proper type
119
+ */
120
+ optionalReference: (name: string) =>
121
+ tagColumnSchema(varchar(name, { length: 256 }).$type<TId>(), schema),
122
+
123
+ /**
124
+ * Drizzle column helper for foreign key reference with relation
125
+ * Configures as varchar(256), not null, with proper type and foreign key constraint
126
+ */
127
+ foreignKey: (
128
+ name: string,
129
+ referenceFn: () => PgColumn,
130
+ options?: { onDelete?: 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default' },
131
+ ) =>
132
+ tagColumnSchema(
133
+ varchar(name, { length: 256 }).notNull().$type<TId>().references(referenceFn, options),
134
+ schema,
135
+ ),
136
+
137
+ /**
138
+ * Drizzle column helper for optional foreign key reference with relation
139
+ * Configures as varchar(256), nullable, with proper type and foreign key constraint
140
+ */
141
+ optionalForeignKey: (
142
+ name: string,
143
+ referenceFn: () => PgColumn,
144
+ options?: { onDelete?: 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default' },
145
+ ) =>
146
+ tagColumnSchema(
147
+ varchar(name, { length: 256 }).$type<TId>().references(referenceFn, options),
148
+ schema,
149
+ ),
150
+
151
+ /**
152
+ * Creates a foreign key factory bound to a specific table
153
+ * This allows creating multiple foreign keys to the same table without repeating the reference
154
+ */
155
+ createForeignKeyFactory: (referenceFn: () => PgColumn) => ({
156
+ required: (
157
+ name: string,
158
+ options?: { onDelete?: 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default' },
159
+ ) => factory.foreignKey(name, referenceFn, options),
160
+ optional: (
161
+ name: string,
162
+ options?: { onDelete?: 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default' },
163
+ ) => factory.optionalForeignKey(name, referenceFn, options),
164
+ }),
165
+ };
166
+
167
+ return factory;
168
+ }
169
+
170
+ // Re-export drizzle and zod utilities
171
+ export * from 'drizzle-orm';
172
+ export { createInsertSchema, createSelectSchema, createSchemaFactory, createUpdateSchema } from 'drizzle-orm/zod';
package/src/id.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { createId as createCuid } from '@paralleldrive/cuid2';
2
+
3
+ export const createId = () => {
4
+ return createCuid();
5
+ };
6
+
7
+ export const createPrefixedId = <T extends string>(prefix: T): `${T}${string}` => {
8
+ return `${prefix}${createId()}`;
9
+ };
10
+
11
+ // Re-export the new ID factory
12
+ export * from './id-factory.js';
@@ -0,0 +1,58 @@
1
+ import type { AnyRelations } from 'drizzle-orm';
2
+ import type { DrizzlePgConfig } from 'drizzle-orm/pg-core/utils';
3
+ import { drizzle } from 'drizzle-orm/bun-sql/postgres';
4
+ import { SQL } from 'bun';
5
+
6
+ import { debugInit } from './debug.js';
7
+
8
+ debugInit('[init.bun] Using Bun SQL (native) for database connections');
9
+
10
+ export const initDb = <TRelations extends AnyRelations>(
11
+ connectionString: string,
12
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
13
+ ) => {
14
+ debugInit('[init.bun] Initializing database with connection string (logger: %s)', !!config.logger);
15
+
16
+ const client = new SQL(connectionString);
17
+ const db = drizzle<TRelations>({ ...config, client });
18
+
19
+ debugInit('[init.bun] Database initialized successfully');
20
+
21
+ return db;
22
+ };
23
+
24
+ export const initDbWithOptions = <TRelations extends AnyRelations>(
25
+ poolConfig: {
26
+ connectionString?: string;
27
+ host?: string;
28
+ port?: number;
29
+ database?: string;
30
+ user?: string;
31
+ password?: string;
32
+ min?: number;
33
+ max?: number;
34
+ },
35
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
36
+ ) => {
37
+ debugInit('[init.bun] Initializing database with pool config (min: %d, max: %d, logger: %s)',
38
+ poolConfig.min ?? 0, poolConfig.max ?? 10, !!config.logger);
39
+
40
+ const client = poolConfig.connectionString
41
+ ? new SQL(poolConfig.connectionString)
42
+ : new SQL({
43
+ hostname: poolConfig.host,
44
+ port: poolConfig.port,
45
+ database: poolConfig.database,
46
+ username: poolConfig.user,
47
+ password: poolConfig.password,
48
+ });
49
+
50
+ const db = drizzle<TRelations>({ ...config, client });
51
+
52
+ debugInit('[init.bun] Database initialized with custom pool configuration');
53
+
54
+ return db;
55
+ };
56
+
57
+ export { createDb } from './factory.js';
58
+ export { getDbConnectionOptions, getDbConnectionParams } from './connection-options.js';
package/src/init.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { AnyRelations } from 'drizzle-orm';
2
+ import type { DrizzlePgConfig } from 'drizzle-orm/pg-core/utils';
3
+ import type { PoolConfig } from 'pg';
4
+ import { drizzle } from 'drizzle-orm/node-postgres';
5
+ import pg from 'pg';
6
+
7
+ import { debugInit } from './debug.js';
8
+
9
+ const Pool = pg.Pool;
10
+ debugInit('[init] Using pg (pure JS) for database connections');
11
+
12
+ export const initDb = <TRelations extends AnyRelations>(
13
+ connectionString: string,
14
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
15
+ ) => {
16
+ debugInit('[init] Initializing database with connection string (logger: %s)', !!config.logger);
17
+
18
+ const client = new Pool({
19
+ connectionString,
20
+ });
21
+
22
+ const db = drizzle<TRelations>({ ...config, client });
23
+ debugInit('[init] Database initialized successfully');
24
+
25
+ return db;
26
+ };
27
+
28
+ export const initDbWithOptions = <TRelations extends AnyRelations>(
29
+ poolConfig: PoolConfig,
30
+ config: Omit<DrizzlePgConfig<TRelations>, 'relations'> & { relations: TRelations },
31
+ ) => {
32
+ debugInit(
33
+ '[init] Initializing database with pool config (min: %d, max: %d, logger: %s)',
34
+ poolConfig.min ?? 0,
35
+ poolConfig.max ?? 10,
36
+ !!config.logger,
37
+ );
38
+
39
+ const client = new Pool(poolConfig);
40
+
41
+ const db = drizzle<TRelations>({ ...config, client });
42
+ debugInit('[init] Database initialized with custom pool configuration');
43
+
44
+ return db;
45
+ };
46
+
47
+ export { createDb } from './factory.js';
48
+ export { getDbConnectionOptions, getDbConnectionParams } from './connection-options.js';
package/src/orm.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from 'drizzle-orm';
2
+ // `drizzle-orm/zod` replaces the standalone drizzle-zod package in 1.0.
3
+ // Named re-export prevents an IsNever collision with drizzle-orm's main types.
4
+ export {
5
+ createInsertSchema,
6
+ createSchemaFactory,
7
+ createSelectSchema,
8
+ createUpdateSchema,
9
+ } from 'drizzle-orm/zod';
package/src/pg.ts ADDED
@@ -0,0 +1 @@
1
+ export * from 'drizzle-orm/pg-core';
@@ -0,0 +1,48 @@
1
+ import type { AnyRelations } from 'drizzle-orm';
2
+ import { PGlite, type Extension } from '@electric-sql/pglite';
3
+ import { drizzle, type PgliteDatabase } from 'drizzle-orm/pglite';
4
+
5
+ import { debugInit } from './debug.js';
6
+
7
+ export type PgliteInitOptions = {
8
+ /** Path to the PGlite data directory. Defaults to `./.local/db`. */
9
+ dataDir?: string;
10
+ /** Use an in-memory database (overrides dataDir). */
11
+ memory?: boolean;
12
+ /** PGlite extensions to load (e.g. `{ vector }` from `@electric-sql/pglite/vector`). */
13
+ extensions?: Record<string, Extension>;
14
+ };
15
+
16
+ /**
17
+ * Initialize a PGlite-backed Drizzle database for embedded local development.
18
+ *
19
+ * Returns the raw PGlite instance alongside the Drizzle handle so callers can
20
+ * share it with `@arki/queue/runtime-local` (one process, one PGlite).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { initDbRuntimeLocal } from '@arki/db/runtime-local';
25
+ * const { db, pglite } = await initDbRuntimeLocal({ dataDir: './.local/db' }, { relations });
26
+ * ```
27
+ */
28
+ export async function initDbRuntimeLocal<TRelations extends AnyRelations>(
29
+ options: PgliteInitOptions,
30
+ config: { relations: TRelations },
31
+ ): Promise<{ db: PgliteDatabase<TRelations>; pglite: PGlite }> {
32
+ const url = options.memory === true ? 'memory://' : (options.dataDir ?? './.local/db');
33
+ debugInit('[runtime-local] Initializing PGlite at: %s', url);
34
+
35
+ const pglite = options.extensions
36
+ ? new PGlite(url, { extensions: options.extensions })
37
+ : new PGlite(url);
38
+ await pglite.waitReady;
39
+ debugInit('[runtime-local] PGlite ready');
40
+
41
+ const db = drizzle({
42
+ client: pglite,
43
+ relations: config.relations,
44
+ }) as PgliteDatabase<TRelations>;
45
+
46
+ debugInit('[runtime-local] Drizzle database initialized');
47
+ return { db, pglite };
48
+ }