turbine-orm 0.5.0 → 0.7.1

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.
Files changed (50) hide show
  1. package/README.md +292 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +311 -43
  4. package/dist/cjs/cli/loader.js +129 -0
  5. package/dist/cjs/cli/migrate.js +96 -47
  6. package/dist/cjs/cli/ui.js +5 -9
  7. package/dist/cjs/client.js +158 -49
  8. package/dist/cjs/errors.js +424 -0
  9. package/dist/cjs/generate.js +145 -14
  10. package/dist/cjs/index.js +43 -20
  11. package/dist/cjs/introspect.js +3 -5
  12. package/dist/cjs/pipeline.js +9 -2
  13. package/dist/cjs/query.js +544 -115
  14. package/dist/cjs/schema-builder.js +150 -30
  15. package/dist/cjs/schema-sql.js +241 -37
  16. package/dist/cjs/schema.js +5 -2
  17. package/dist/cjs/serverless.js +88 -176
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +316 -48
  20. package/dist/cli/loader.d.ts +45 -0
  21. package/dist/cli/loader.js +91 -0
  22. package/dist/cli/migrate.d.ts +13 -2
  23. package/dist/cli/migrate.js +97 -48
  24. package/dist/cli/ui.d.ts +1 -1
  25. package/dist/cli/ui.js +5 -9
  26. package/dist/client.d.ts +92 -4
  27. package/dist/client.js +158 -49
  28. package/dist/errors.d.ts +225 -0
  29. package/dist/errors.js +405 -0
  30. package/dist/generate.d.ts +7 -1
  31. package/dist/generate.js +148 -18
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +16 -12
  34. package/dist/introspect.d.ts +1 -1
  35. package/dist/introspect.js +4 -6
  36. package/dist/pipeline.d.ts +1 -1
  37. package/dist/pipeline.js +9 -2
  38. package/dist/query.d.ts +374 -38
  39. package/dist/query.js +545 -116
  40. package/dist/schema-builder.d.ts +38 -5
  41. package/dist/schema-builder.js +150 -31
  42. package/dist/schema-sql.d.ts +7 -3
  43. package/dist/schema-sql.js +241 -37
  44. package/dist/schema.d.ts +1 -1
  45. package/dist/schema.js +5 -2
  46. package/dist/serverless.d.ts +92 -139
  47. package/dist/serverless.js +87 -173
  48. package/package.json +33 -16
  49. package/dist/types.d.ts +0 -93
  50. package/dist/types.js +0 -126
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — Schema metadata types
3
+ * turbine-orm — Schema metadata types
4
4
  *
5
5
  * These types represent the introspected database schema at runtime.
6
6
  * They're used by the query builder, code generator, and CLI.
@@ -20,6 +20,9 @@ const PG_TO_TS = {
20
20
  // Integers
21
21
  int2: 'number',
22
22
  int4: 'number',
23
+ // int8 maps to `number` for DX (auto-increment IDs, counts, etc.).
24
+ // Values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1) are returned as
25
+ // `string` at runtime to avoid precision loss. See client.ts int8 parser.
23
26
  int8: 'number',
24
27
  float4: 'number',
25
28
  float8: 'number',
@@ -128,7 +131,7 @@ function snakeToPascal(s) {
128
131
  /** Naive singularize: "posts" → "post", "categories" → "category" */
129
132
  function singularize(s) {
130
133
  if (s.endsWith('ies'))
131
- return s.slice(0, -3) + 'y';
134
+ return `${s.slice(0, -3)}y`;
132
135
  if (s.endsWith('ses') || s.endsWith('xes') || s.endsWith('zes'))
133
136
  return s.slice(0, -2);
134
137
  if (s.endsWith('s') && !s.endsWith('ss'))
@@ -1,199 +1,111 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine/serverless — HTTP-based query driver for edge functions
3
+ * turbine-orm/serverless — edge / serverless driver integration
4
4
  *
5
- * Use this driver when you cannot establish a direct TCP connection to Postgres
6
- * (e.g., Vercel Edge Functions, Cloudflare Workers, Deno Deploy).
5
+ * Turbine runs on any Postgres driver that speaks the node-postgres API.
6
+ * This module exposes a thin factory (`turbineHttp`) that binds an external
7
+ * pg-compatible pool to a schema, so you can use Turbine on Vercel Edge,
8
+ * Cloudflare Workers, Deno Deploy, Netlify Edge, or any other environment
9
+ * where a direct TCP connection is unavailable.
7
10
  *
8
- * It sends queries as JSON over HTTP to a Turbine query endpoint, which executes
9
- * them against the actual database and returns typed results.
11
+ * ## Supported drivers
10
12
  *
11
- * NOTE: This is a scaffold. The server-side query endpoint does not exist yet.
12
- * The HTTP protocol and response format are defined here and will be implemented
13
- * on the server side in a future release.
13
+ * Any driver whose `Pool` satisfies `PgCompatPool` will work. The ones
14
+ * below are verified:
15
+ *
16
+ * - **Neon** (`@neondatabase/serverless`) — HTTP and WebSocket transports
17
+ * - **Vercel Postgres** (`@vercel/postgres`) — wraps Neon
18
+ * - **Cloudflare Hyperdrive** — exposes a pg-compatible driver
19
+ * - **Supabase** — use the regular `pg` package; Supabase is Postgres-native
20
+ *
21
+ * Turbine does NOT bundle any of these — install whichever you need and
22
+ * pass its pool directly.
23
+ *
24
+ * ## Limitations over HTTP
25
+ *
26
+ * - **Streaming cursors** (`findManyStream`, `findManyCursor`) require
27
+ * server-side `DECLARE CURSOR`, which most HTTP drivers do not support.
28
+ * If you call these on an HTTP pool the underlying driver will error.
29
+ * - **LISTEN/NOTIFY** is not available over HTTP.
30
+ * - **Transactions** are supported but each transaction holds an HTTP
31
+ * connection for its duration — keep them short.
32
+ *
33
+ * ## Example — Neon on Vercel Edge
34
+ *
35
+ * ```ts
36
+ * // app/api/users/route.ts
37
+ * import { Pool } from '@neondatabase/serverless';
38
+ * import { turbineHttp } from 'turbine-orm/serverless';
39
+ * import { SCHEMA } from '../../generated/turbine/metadata';
40
+ *
41
+ * export const runtime = 'edge';
42
+ *
43
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
44
+ * const db = turbineHttp(pool, SCHEMA);
45
+ *
46
+ * export async function GET() {
47
+ * const users = await db.table('users').findMany({ limit: 10 });
48
+ * return Response.json(users);
49
+ * }
50
+ * ```
51
+ *
52
+ * ## Example — Supabase (direct Postgres, no HTTP proxy needed)
14
53
  *
15
- * @example
16
54
  * ```ts
17
- * import { createServerlessClient } from '@batadata/turbine/serverless';
55
+ * import { TurbineClient } from 'turbine-orm';
56
+ * import { SCHEMA } from './generated/turbine/metadata.js';
57
+ *
58
+ * const db = new TurbineClient({
59
+ * connectionString: process.env.SUPABASE_DB_URL,
60
+ * ssl: { rejectUnauthorized: false },
61
+ * }, SCHEMA);
62
+ * ```
18
63
  *
19
- * const db = createServerlessClient({
20
- * endpoint: 'https://your-turbine-proxy.fly.dev/query',
21
- * authToken: process.env.TURBINE_AUTH_TOKEN!,
22
- * });
64
+ * ## Example Cloudflare Workers
23
65
  *
24
- * const result = await db.query('SELECT * FROM users WHERE id = $1', [1]);
66
+ * ```ts
67
+ * // Use the Neon HTTP driver which works in Workers runtime
68
+ * import { Pool } from '@neondatabase/serverless';
69
+ * import { turbineHttp } from 'turbine-orm/serverless';
70
+ * import { SCHEMA } from './generated/turbine/metadata';
71
+ *
72
+ * export default {
73
+ * async fetch(req: Request, env: Env) {
74
+ * const pool = new Pool({ connectionString: env.DATABASE_URL });
75
+ * const db = turbineHttp(pool, SCHEMA);
76
+ * const users = await db.table('users').findMany({ limit: 10 });
77
+ * return Response.json(users);
78
+ * }
79
+ * };
25
80
  * ```
26
81
  */
27
82
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.ServerlessClient = void 0;
29
- exports.createServerlessClient = createServerlessClient;
30
- // ---------------------------------------------------------------------------
31
- // Serverless client
32
- // ---------------------------------------------------------------------------
83
+ exports.turbineHttp = turbineHttp;
84
+ const client_js_1 = require("./client.js");
33
85
  /**
34
- * HTTP-based Postgres query client for serverless/edge environments.
86
+ * Create a TurbineClient bound to an external pg-compatible pool.
35
87
  *
36
- * Sends SQL queries as JSON POST requests to a Turbine query endpoint.
37
- * Does not require a direct TCP connection to Postgres.
38
- */
39
- class ServerlessClient {
40
- config;
41
- fetchFn;
42
- constructor(config) {
43
- if (!config.endpoint) {
44
- throw new Error('[turbine/serverless] endpoint is required');
45
- }
46
- if (!config.authToken) {
47
- throw new Error('[turbine/serverless] authToken is required');
48
- }
49
- this.config = {
50
- ...config,
51
- timeout: config.timeout ?? 10_000,
52
- };
53
- this.fetchFn = config.fetch ?? globalThis.fetch;
54
- }
55
- /**
56
- * Execute a single SQL query.
57
- *
58
- * @param sql - SQL string with $1, $2, ... placeholders
59
- * @param params - Parameter values
60
- * @returns Query result with typed rows
61
- *
62
- * @example
63
- * ```ts
64
- * const result = await client.query<{ id: number; name: string }>(
65
- * 'SELECT id, name FROM users WHERE org_id = $1',
66
- * [42]
67
- * );
68
- * console.log(result.rows);
69
- * ```
70
- */
71
- async query(sql, params) {
72
- const request = { sql, params, mode: 'rows' };
73
- const response = await this.post('/query', request);
74
- return response;
75
- }
76
- /**
77
- * Execute a single SQL query and return the first row, or null.
78
- */
79
- async queryOne(sql, params) {
80
- const request = { sql, params, mode: 'one' };
81
- const response = await this.post('/query', request);
82
- return response.rows[0] ?? null;
83
- }
84
- /**
85
- * Execute a batch of queries in a single HTTP request.
86
- * Optionally wraps them in a transaction.
87
- *
88
- * @param queries - Array of queries to execute
89
- * @param options - Batch options
90
- * @returns Array of results, one per query
91
- *
92
- * @example
93
- * ```ts
94
- * const results = await client.batch([
95
- * { sql: 'SELECT * FROM users WHERE id = $1', params: [1] },
96
- * { sql: 'SELECT COUNT(*) FROM posts WHERE user_id = $1', params: [1] },
97
- * ], { transaction: true });
98
- * ```
99
- */
100
- async batch(queries, options) {
101
- const request = {
102
- queries,
103
- transaction: options?.transaction ?? false,
104
- };
105
- return this.post('/batch', request);
106
- }
107
- /**
108
- * Tagged template helper for SQL queries.
109
- *
110
- * @example
111
- * ```ts
112
- * const users = await client.sql<{ id: number; name: string }>`
113
- * SELECT id, name FROM users WHERE org_id = ${orgId}
114
- * `;
115
- * ```
116
- */
117
- async sql(strings, ...values) {
118
- let sqlStr = '';
119
- strings.forEach((str, i) => {
120
- sqlStr += str;
121
- if (i < values.length) {
122
- sqlStr += `$${i + 1}`;
123
- }
124
- });
125
- const result = await this.query(sqlStr, values);
126
- return result.rows;
127
- }
128
- // -------------------------------------------------------------------------
129
- // Internal HTTP transport
130
- // -------------------------------------------------------------------------
131
- async post(path, body) {
132
- const url = this.config.endpoint.replace(/\/$/, '') + path;
133
- const controller = new AbortController();
134
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
135
- try {
136
- const response = await this.fetchFn(url, {
137
- method: 'POST',
138
- headers: {
139
- 'Content-Type': 'application/json',
140
- 'Authorization': `Bearer ${this.config.authToken}`,
141
- 'User-Agent': '@batadata/turbine-serverless',
142
- ...this.config.headers,
143
- },
144
- body: JSON.stringify(body),
145
- signal: controller.signal,
146
- });
147
- if (!response.ok) {
148
- const errorBody = await response.text();
149
- let parsed;
150
- try {
151
- parsed = JSON.parse(errorBody);
152
- }
153
- catch {
154
- // Not JSON
155
- }
156
- const message = parsed?.error ?? `HTTP ${response.status}: ${errorBody.slice(0, 200)}`;
157
- const err = new Error(`[turbine/serverless] ${message}`);
158
- err['status'] = response.status;
159
- err['code'] = parsed?.code;
160
- throw err;
161
- }
162
- return (await response.json());
163
- }
164
- catch (err) {
165
- if (err instanceof DOMException && err.name === 'AbortError') {
166
- throw new Error(`[turbine/serverless] Request timed out after ${this.config.timeout}ms`);
167
- }
168
- throw err;
169
- }
170
- finally {
171
- clearTimeout(timeoutId);
172
- }
173
- }
174
- }
175
- exports.ServerlessClient = ServerlessClient;
176
- // ---------------------------------------------------------------------------
177
- // Factory function
178
- // ---------------------------------------------------------------------------
179
- /**
180
- * Create a serverless Turbine client for edge/serverless environments.
88
+ * Use this for serverless/edge environments where Turbine should NOT
89
+ * manage its own `pg.Pool`. The caller retains ownership of the pool's
90
+ * lifecycle — `db.disconnect()` is a no-op.
181
91
  *
182
- * @param config - Endpoint URL and auth token
183
- * @returns A ServerlessClient instance
92
+ * @param pool - Any pg-compatible pool (Neon, Vercel Postgres, etc.)
93
+ * @param schema - Introspected or hand-written schema metadata
94
+ * @param options - Optional logging / defaultLimit / warnOnUnlimited
95
+ * @returns A TurbineClient instance
184
96
  *
185
97
  * @example
186
98
  * ```ts
187
- * import { createServerlessClient } from '@batadata/turbine/serverless';
99
+ * import { Pool } from '@neondatabase/serverless';
100
+ * import { turbineHttp } from 'turbine-orm/serverless';
101
+ * import { SCHEMA } from './generated/turbine/metadata.js';
188
102
  *
189
- * const db = createServerlessClient({
190
- * endpoint: process.env.TURBINE_ENDPOINT!,
191
- * authToken: process.env.TURBINE_AUTH_TOKEN!,
192
- * });
103
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
104
+ * const db = turbineHttp(pool, SCHEMA);
193
105
  *
194
- * const users = await db.sql`SELECT * FROM users LIMIT 10`;
106
+ * const users = await db.table('users').findMany({ limit: 10 });
195
107
  * ```
196
108
  */
197
- function createServerlessClient(config) {
198
- return new ServerlessClient(config);
109
+ function turbineHttp(pool, schema, options = {}) {
110
+ return new client_js_1.TurbineClient({ pool, ...options }, schema);
199
111
  }
@@ -5,17 +5,12 @@
5
5
  * Falls back to CLI args and environment variables.
6
6
  */
7
7
  import { existsSync } from 'node:fs';
8
- import { resolve, join } from 'node:path';
8
+ import { join, resolve } from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
10
  // ---------------------------------------------------------------------------
11
11
  // Config file names, in priority order
12
12
  // ---------------------------------------------------------------------------
13
- const CONFIG_FILES = [
14
- 'turbine.config.ts',
15
- 'turbine.config.mts',
16
- 'turbine.config.js',
17
- 'turbine.config.mjs',
18
- ];
13
+ const CONFIG_FILES = ['turbine.config.ts', 'turbine.config.mts', 'turbine.config.js', 'turbine.config.mjs'];
19
14
  // ---------------------------------------------------------------------------
20
15
  // Load config
21
16
  // ---------------------------------------------------------------------------
@@ -67,10 +62,7 @@ export function findConfigFile(cwd) {
67
62
  */
68
63
  export function resolveConfig(fileConfig, overrides) {
69
64
  return {
70
- url: overrides.url ??
71
- process.env['DATABASE_URL'] ??
72
- fileConfig.url ??
73
- '',
65
+ url: overrides.url ?? process.env.DATABASE_URL ?? fileConfig.url ?? '',
74
66
  out: overrides.out ?? fileConfig.out ?? './generated/turbine',
75
67
  schema: overrides.schema ?? fileConfig.schema ?? 'public',
76
68
  include: overrides.include ?? fileConfig.include ?? [],
@@ -84,15 +76,13 @@ export function resolveConfig(fileConfig, overrides) {
84
76
  // Config file template (for `turbine init`)
85
77
  // ---------------------------------------------------------------------------
86
78
  export function configTemplate(connectionString) {
87
- const url = connectionString ?? 'process.env.DATABASE_URL';
88
- const urlLine = connectionString
89
- ? ` url: '${connectionString}',`
90
- : ` url: process.env.DATABASE_URL,`;
79
+ const _url = connectionString ?? 'process.env.DATABASE_URL';
80
+ const urlLine = connectionString ? ` url: '${connectionString}',` : ` url: process.env.DATABASE_URL,`;
91
81
  return `import type { TurbineCliConfig } from 'turbine-orm/cli';
92
82
 
93
83
  /**
94
84
  * Turbine configuration
95
- * @see https://batadata.com/docs/turbine/config
85
+ * @see https://turbineorm.dev
96
86
  */
97
87
  const config: TurbineCliConfig = {
98
88
  /** Postgres connection string */