@arki/db 0.1.0 → 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/dist/client/ids.d.ts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/dot.d.ts +28 -12
- package/dist/dot.d.ts.map +1 -1
- package/dist/dot.js +49 -18
- package/dist/dot.js.map +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +14 -10
- package/dist/env.js.map +1 -1
- package/package.json +12 -5
- package/src/builder.ts +359 -0
- package/src/bun.ts +58 -0
- package/src/client/README.md +5 -0
- package/src/client/ids.ts +11 -0
- package/src/client/index.ts +11 -0
- package/src/connection-options.ts +49 -0
- package/src/debug.ts +24 -0
- package/src/dot.ts +200 -0
- package/src/env.ts +92 -0
- package/src/factory.ts +47 -0
- package/src/id-factory.ts +172 -0
- package/src/id.ts +12 -0
- package/src/init.bun.ts +58 -0
- package/src/init.ts +48 -0
- package/src/orm.ts +9 -0
- package/src/pg.ts +1 -0
- package/src/runtime-local.ts +48 -0
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';
|
package/src/init.bun.ts
ADDED
|
@@ -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';
|