turbine-orm 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/cjs/cli/config.js +161 -0
- package/dist/cjs/cli/index.js +977 -0
- package/dist/cjs/cli/migrate.js +421 -0
- package/dist/cjs/cli/ui.js +237 -0
- package/dist/cjs/client.js +449 -0
- package/dist/cjs/generate.js +301 -0
- package/dist/cjs/index.js +75 -0
- package/dist/cjs/introspect.js +289 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +71 -0
- package/dist/cjs/query.js +1558 -0
- package/dist/cjs/schema-builder.js +169 -0
- package/dist/cjs/schema-sql.js +371 -0
- package/dist/cjs/schema.js +137 -0
- package/dist/cjs/serverless.js +199 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/index.js +16 -8
- package/dist/cli/migrate.d.ts +29 -5
- package/dist/cli/migrate.js +58 -35
- package/dist/cli/ui.js +1 -1
- package/dist/client.d.ts +15 -4
- package/dist/client.js +28 -15
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +13 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +1 -1
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +1 -1
- package/dist/query.d.ts +55 -11
- package/dist/query.js +135 -140
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +2 -2
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +31 -15
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/serverless.d.ts +3 -3
- package/dist/serverless.js +4 -4
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +17 -11
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @batadata/turbine/serverless — HTTP-based query driver for edge functions
|
|
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).
|
|
7
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createServerlessClient } from '@batadata/turbine/serverless';
|
|
18
|
+
*
|
|
19
|
+
* const db = createServerlessClient({
|
|
20
|
+
* endpoint: 'https://your-turbine-proxy.fly.dev/query',
|
|
21
|
+
* authToken: process.env.TURBINE_AUTH_TOKEN!,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await db.query('SELECT * FROM users WHERE id = $1', [1]);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.ServerlessClient = void 0;
|
|
29
|
+
exports.createServerlessClient = createServerlessClient;
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Serverless client
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* HTTP-based Postgres query client for serverless/edge environments.
|
|
35
|
+
*
|
|
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.
|
|
181
|
+
*
|
|
182
|
+
* @param config - Endpoint URL and auth token
|
|
183
|
+
* @returns A ServerlessClient instance
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { createServerlessClient } from '@batadata/turbine/serverless';
|
|
188
|
+
*
|
|
189
|
+
* const db = createServerlessClient({
|
|
190
|
+
* endpoint: process.env.TURBINE_ENDPOINT!,
|
|
191
|
+
* authToken: process.env.TURBINE_AUTH_TOKEN!,
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* const users = await db.sql`SELECT * FROM users LIMIT 10`;
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
function createServerlessClient(config) {
|
|
198
|
+
return new ServerlessClient(config);
|
|
199
|
+
}
|
package/dist/cli/config.js
CHANGED
|
@@ -92,7 +92,7 @@ export function configTemplate(connectionString) {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Turbine configuration
|
|
95
|
-
* @see https://
|
|
95
|
+
* @see https://batadata.com/docs/turbine/config
|
|
96
96
|
*/
|
|
97
97
|
const config: TurbineCliConfig = {
|
|
98
98
|
/** Postgres connection string */
|
package/dist/cli/index.js
CHANGED
|
@@ -231,7 +231,7 @@ async function cmdInit(args, config) {
|
|
|
231
231
|
* Define your database schema in TypeScript.
|
|
232
232
|
* Use \`npx turbine push\` to sync it to your database.
|
|
233
233
|
*
|
|
234
|
-
* @see https://
|
|
234
|
+
* @see https://batadata.com/docs/turbine/schema
|
|
235
235
|
*/
|
|
236
236
|
|
|
237
237
|
import { defineSchema } from 'turbine-orm';
|
|
@@ -248,13 +248,20 @@ export default defineSchema({
|
|
|
248
248
|
`, 'utf-8');
|
|
249
249
|
success(`Created ${cyan(config.schemaFile)}`);
|
|
250
250
|
}
|
|
251
|
-
// Add .gitignore
|
|
251
|
+
// Add .gitignore entries for generated output and config (may contain connection strings)
|
|
252
252
|
const gitignorePath = '.gitignore';
|
|
253
253
|
if (existsSync(gitignorePath)) {
|
|
254
254
|
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
255
|
+
const additions = [];
|
|
255
256
|
if (!gitignoreContent.includes('generated/turbine')) {
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
additions.push('generated/turbine/');
|
|
258
|
+
}
|
|
259
|
+
if (!gitignoreContent.includes('turbine.config.ts')) {
|
|
260
|
+
additions.push('turbine.config.ts');
|
|
261
|
+
}
|
|
262
|
+
if (additions.length > 0) {
|
|
263
|
+
appendFileSync(gitignorePath, `\n# Turbine generated client & config\n${additions.join('\n')}\n`);
|
|
264
|
+
success(`Added ${cyan(additions.join(', '))} to ${cyan('.gitignore')}`);
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
267
|
// If we have a URL, run initial generate
|
|
@@ -669,10 +676,12 @@ async function cmdSeed(args, config) {
|
|
|
669
676
|
{ cmd: 'npx tsx', name: 'tsx' },
|
|
670
677
|
{ cmd: 'node --experimental-strip-types', name: 'node' },
|
|
671
678
|
];
|
|
679
|
+
// Shell-escape the seed file path to prevent injection
|
|
680
|
+
const escapedSeedFile = seedFile.replace(/'/g, "'\\''");
|
|
672
681
|
let ran = false;
|
|
673
682
|
for (const runner of runners) {
|
|
674
683
|
try {
|
|
675
|
-
execSync(`${runner.cmd} ${
|
|
684
|
+
execSync(`${runner.cmd} '${escapedSeedFile}'`, {
|
|
676
685
|
stdio: 'inherit',
|
|
677
686
|
env: {
|
|
678
687
|
...process.env,
|
|
@@ -768,7 +777,7 @@ async function cmdStudio(_args, _config) {
|
|
|
768
777
|
'A local web UI for browsing your database,',
|
|
769
778
|
'exploring relations, and managing data.',
|
|
770
779
|
'',
|
|
771
|
-
`Follow ${cyan('@
|
|
780
|
+
`Follow ${cyan('@batadata')} for updates.`,
|
|
772
781
|
].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
|
|
773
782
|
newline();
|
|
774
783
|
}
|
|
@@ -819,8 +828,7 @@ function showHelp() {
|
|
|
819
828
|
// Version
|
|
820
829
|
// ---------------------------------------------------------------------------
|
|
821
830
|
function showVersion() {
|
|
822
|
-
|
|
823
|
-
console.log(`turbine-orm v0.3.0`);
|
|
831
|
+
console.log(`turbine-orm v0.5.0`);
|
|
824
832
|
}
|
|
825
833
|
// ---------------------------------------------------------------------------
|
|
826
834
|
// Main
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -12,16 +12,14 @@
|
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
14
|
export interface MigrationFile {
|
|
15
|
-
/** Full filename (e.g. "
|
|
15
|
+
/** Full filename (e.g. "20260325120000_create_users.sql") */
|
|
16
16
|
filename: string;
|
|
17
17
|
/** Absolute path to the file */
|
|
18
18
|
path: string;
|
|
19
|
-
/** Extracted name portion (e.g. "
|
|
19
|
+
/** Extracted name portion (e.g. "20260325120000_create_users") */
|
|
20
20
|
name: string;
|
|
21
|
-
/** Timestamp prefix (e.g. "
|
|
21
|
+
/** Timestamp prefix (e.g. "20260325120000") — YYYYMMDDHHMMSS */
|
|
22
22
|
timestamp: string;
|
|
23
|
-
/** Sequence number (e.g. "001") */
|
|
24
|
-
sequence: string;
|
|
25
23
|
}
|
|
26
24
|
export interface AppliedMigration {
|
|
27
25
|
id: number;
|
|
@@ -36,10 +34,36 @@ export interface MigrationStatus {
|
|
|
36
34
|
/** True if the file checksum matches the stored checksum (only set for applied migrations) */
|
|
37
35
|
checksumValid?: boolean;
|
|
38
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse a migration filename into its components.
|
|
39
|
+
* Expected format: YYYYMMDDHHMMSS_description.sql
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseMigrationFilename(filename: string): MigrationFile | null;
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sanitizeName(name: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Generate a YYYYMMDDHHMMSS timestamp string from a Date.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatTimestamp(date: Date): string;
|
|
50
|
+
/**
|
|
51
|
+
* Get pending migration files — those not yet applied.
|
|
52
|
+
* Returns files sorted by timestamp (ascending).
|
|
53
|
+
*/
|
|
54
|
+
export declare function getPendingMigrations(migrationsDir: string, applied: string[]): MigrationFile[];
|
|
39
55
|
/**
|
|
40
56
|
* List all migration files in the migrations directory, sorted by name.
|
|
41
57
|
*/
|
|
42
58
|
export declare function listMigrationFiles(migrationsDir: string): MigrationFile[];
|
|
59
|
+
/**
|
|
60
|
+
* Parse migration content string into UP and DOWN sections.
|
|
61
|
+
* Exported for unit testing.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseMigrationContent(content: string): {
|
|
64
|
+
up: string;
|
|
65
|
+
down: string;
|
|
66
|
+
};
|
|
43
67
|
/**
|
|
44
68
|
* Parse a migration file into UP and DOWN sections.
|
|
45
69
|
*/
|
package/dist/cli/migrate.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
15
|
-
import { join
|
|
15
|
+
import { join } from 'node:path';
|
|
16
16
|
import pg from 'pg';
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Tracking table management
|
|
@@ -39,10 +39,10 @@ async function getAppliedMigrations(client) {
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
/**
|
|
41
41
|
* Parse a migration filename into its components.
|
|
42
|
-
* Expected format:
|
|
42
|
+
* Expected format: YYYYMMDDHHMMSS_description.sql
|
|
43
43
|
*/
|
|
44
|
-
function parseMigrationFilename(filename) {
|
|
45
|
-
const match = filename.match(/^(\d{
|
|
44
|
+
export function parseMigrationFilename(filename) {
|
|
45
|
+
const match = filename.match(/^(\d{14})_(.+)\.sql$/);
|
|
46
46
|
if (!match)
|
|
47
47
|
return null;
|
|
48
48
|
return {
|
|
@@ -50,9 +50,39 @@ function parseMigrationFilename(filename) {
|
|
|
50
50
|
path: '', // Set by caller
|
|
51
51
|
name: filename.replace(/\.sql$/, ''),
|
|
52
52
|
timestamp: match[1],
|
|
53
|
-
sequence: match[2],
|
|
54
53
|
};
|
|
55
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeName(name) {
|
|
59
|
+
return name
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
62
|
+
.replace(/_+/g, '_')
|
|
63
|
+
.replace(/^_|_$/g, '');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate a YYYYMMDDHHMMSS timestamp string from a Date.
|
|
67
|
+
*/
|
|
68
|
+
export function formatTimestamp(date) {
|
|
69
|
+
return [
|
|
70
|
+
date.getFullYear(),
|
|
71
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
72
|
+
String(date.getDate()).padStart(2, '0'),
|
|
73
|
+
String(date.getHours()).padStart(2, '0'),
|
|
74
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
75
|
+
String(date.getSeconds()).padStart(2, '0'),
|
|
76
|
+
].join('');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get pending migration files — those not yet applied.
|
|
80
|
+
* Returns files sorted by timestamp (ascending).
|
|
81
|
+
*/
|
|
82
|
+
export function getPendingMigrations(migrationsDir, applied) {
|
|
83
|
+
const appliedSet = new Set(applied);
|
|
84
|
+
return listMigrationFiles(migrationsDir).filter((f) => !appliedSet.has(f.name));
|
|
85
|
+
}
|
|
56
86
|
/**
|
|
57
87
|
* List all migration files in the migrations directory, sorted by name.
|
|
58
88
|
*/
|
|
@@ -73,10 +103,10 @@ export function listMigrationFiles(migrationsDir) {
|
|
|
73
103
|
return files;
|
|
74
104
|
}
|
|
75
105
|
/**
|
|
76
|
-
* Parse
|
|
106
|
+
* Parse migration content string into UP and DOWN sections.
|
|
107
|
+
* Exported for unit testing.
|
|
77
108
|
*/
|
|
78
|
-
export function
|
|
79
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
109
|
+
export function parseMigrationContent(content) {
|
|
80
110
|
const lines = content.split('\n');
|
|
81
111
|
let section = 'none';
|
|
82
112
|
const upLines = [];
|
|
@@ -101,6 +131,13 @@ export function parseMigrationSQL(filePath) {
|
|
|
101
131
|
down: downLines.join('\n').trim(),
|
|
102
132
|
};
|
|
103
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse a migration file into UP and DOWN sections.
|
|
136
|
+
*/
|
|
137
|
+
export function parseMigrationSQL(filePath) {
|
|
138
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
139
|
+
return parseMigrationContent(content);
|
|
140
|
+
}
|
|
104
141
|
/**
|
|
105
142
|
* Simple checksum for a migration file (for drift detection).
|
|
106
143
|
*/
|
|
@@ -120,40 +157,26 @@ function checksum(content) {
|
|
|
120
157
|
*/
|
|
121
158
|
export function createMigration(migrationsDir, name) {
|
|
122
159
|
mkdirSync(migrationsDir, { recursive: true });
|
|
123
|
-
// Get today's date as YYYYMMDD
|
|
124
160
|
const now = new Date();
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
String(now.getDate()).padStart(2, '0'),
|
|
129
|
-
].join('');
|
|
130
|
-
// Find the next sequence number for today
|
|
131
|
-
const existing = listMigrationFiles(migrationsDir);
|
|
132
|
-
const todayMigrations = existing.filter((f) => f.timestamp === datePart);
|
|
133
|
-
const nextSeq = String(todayMigrations.length + 1).padStart(3, '0');
|
|
134
|
-
// Sanitize name: lowercase, replace spaces/special chars with underscores
|
|
135
|
-
const safeName = name
|
|
136
|
-
.toLowerCase()
|
|
137
|
-
.replace(/[^a-z0-9_]/g, '_')
|
|
138
|
-
.replace(/_+/g, '_')
|
|
139
|
-
.replace(/^_|_$/g, '');
|
|
140
|
-
const filename = `${datePart}_${nextSeq}_${safeName}.sql`;
|
|
161
|
+
const ts = formatTimestamp(now);
|
|
162
|
+
const safeName = sanitizeName(name);
|
|
163
|
+
const filename = `${ts}_${safeName}.sql`;
|
|
141
164
|
const filePath = join(migrationsDir, filename);
|
|
142
|
-
const template = `--
|
|
143
|
-
--
|
|
165
|
+
const template = `-- Migration: ${name}
|
|
166
|
+
-- Created: ${now.toISOString()}
|
|
144
167
|
|
|
168
|
+
-- UP
|
|
169
|
+
-- Write your migration SQL here
|
|
145
170
|
|
|
146
171
|
-- DOWN
|
|
147
|
-
-- Write
|
|
148
|
-
|
|
172
|
+
-- Write your rollback SQL here
|
|
149
173
|
`;
|
|
150
174
|
writeFileSync(filePath, template, 'utf-8');
|
|
151
175
|
return {
|
|
152
176
|
filename,
|
|
153
177
|
path: filePath,
|
|
154
178
|
name: filename.replace(/\.sql$/, ''),
|
|
155
|
-
timestamp:
|
|
156
|
-
sequence: nextSeq,
|
|
179
|
+
timestamp: ts,
|
|
157
180
|
};
|
|
158
181
|
}
|
|
159
182
|
// ---------------------------------------------------------------------------
|
|
@@ -215,7 +238,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
215
238
|
return {
|
|
216
239
|
applied: [],
|
|
217
240
|
errors: [{
|
|
218
|
-
file: { filename: '', path: '', name: '', timestamp: ''
|
|
241
|
+
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
219
242
|
error: 'Could not acquire migration lock — another migration is already running',
|
|
220
243
|
}],
|
|
221
244
|
};
|
|
@@ -229,7 +252,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
229
252
|
return {
|
|
230
253
|
applied: [],
|
|
231
254
|
errors: [{
|
|
232
|
-
file: { filename: '', path: '', name: '', timestamp: ''
|
|
255
|
+
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
233
256
|
error: `Checksum mismatch: migration file(s) modified after application: ${names}. This is dangerous — applied migrations should be immutable. Use --force to skip this check.`,
|
|
234
257
|
}],
|
|
235
258
|
};
|
|
@@ -293,7 +316,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
293
316
|
return {
|
|
294
317
|
rolledBack: [],
|
|
295
318
|
errors: [{
|
|
296
|
-
file: { filename: '', path: '', name: '', timestamp: ''
|
|
319
|
+
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
297
320
|
error: 'Could not acquire migration lock — another migration is already running',
|
|
298
321
|
}],
|
|
299
322
|
};
|
|
@@ -314,7 +337,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
314
337
|
const file = fileMap.get(migration.name);
|
|
315
338
|
if (!file) {
|
|
316
339
|
errors.push({
|
|
317
|
-
file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: ''
|
|
340
|
+
file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '' },
|
|
318
341
|
error: `Migration file not found for "${migration.name}"`,
|
|
319
342
|
});
|
|
320
343
|
continue;
|
package/dist/cli/ui.js
CHANGED
|
@@ -191,7 +191,7 @@ export function divider() {
|
|
|
191
191
|
// ---------------------------------------------------------------------------
|
|
192
192
|
export function banner() {
|
|
193
193
|
console.log('');
|
|
194
|
-
console.log(` ${bold(cyan('
|
|
194
|
+
console.log(` ${bold(cyan('turbine'))} ${dim('by')} ${bold('BataData')}`);
|
|
195
195
|
console.log(` ${dim('TypeScript ORM with json_agg nested queries')}`);
|
|
196
196
|
console.log('');
|
|
197
197
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine
|
|
2
|
+
* @batadata/turbine — TurbineClient
|
|
3
3
|
*
|
|
4
4
|
* The main entry point for the Turbine TypeScript SDK.
|
|
5
5
|
* Manages connection pooling and provides typed table accessors.
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
17
17
|
*
|
|
18
18
|
* // With base client (dynamic):
|
|
19
|
-
* import { TurbineClient } from 'turbine
|
|
19
|
+
* import { TurbineClient } from '@batadata/turbine';
|
|
20
20
|
* const db = new TurbineClient({ connectionString: '...' }, schema);
|
|
21
21
|
* const users = db.table<User>('users');
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import { QueryInterface, type DeferredQuery } from './query.js';
|
|
25
|
+
import { QueryInterface, type DeferredQuery, type QueryInterfaceOptions } from './query.js';
|
|
26
26
|
import { type PipelineResults } from './pipeline.js';
|
|
27
27
|
import type { SchemaMetadata } from './schema.js';
|
|
28
28
|
export interface TurbineConfig {
|
|
@@ -46,6 +46,10 @@ export interface TurbineConfig {
|
|
|
46
46
|
connectionTimeoutMs?: number;
|
|
47
47
|
/** Enable query logging to console (default: false) */
|
|
48
48
|
logging?: boolean;
|
|
49
|
+
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
|
50
|
+
defaultLimit?: number;
|
|
51
|
+
/** Log a warning when findMany() is called without a limit (default: false) */
|
|
52
|
+
warnOnUnlimited?: boolean;
|
|
49
53
|
}
|
|
50
54
|
/** Parameters passed to middleware functions */
|
|
51
55
|
export interface MiddlewareParams {
|
|
@@ -75,9 +79,10 @@ export declare class TransactionClient {
|
|
|
75
79
|
private readonly client;
|
|
76
80
|
readonly schema: SchemaMetadata;
|
|
77
81
|
private readonly middlewares;
|
|
82
|
+
private readonly queryOptions?;
|
|
78
83
|
private readonly tableCache;
|
|
79
84
|
private savepointCounter;
|
|
80
|
-
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[]);
|
|
85
|
+
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined);
|
|
81
86
|
/**
|
|
82
87
|
* Get a QueryInterface for a table within this transaction.
|
|
83
88
|
* Uses the dedicated transaction connection instead of the pool.
|
|
@@ -107,10 +112,16 @@ export declare class TurbineClient {
|
|
|
107
112
|
private readonly logging;
|
|
108
113
|
private readonly tableCache;
|
|
109
114
|
private readonly middlewares;
|
|
115
|
+
private readonly queryOptions;
|
|
110
116
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
111
117
|
/**
|
|
112
118
|
* Register a middleware function that runs before/after every query.
|
|
113
119
|
*
|
|
120
|
+
* Middleware can inspect and log query parameters, modify results after execution,
|
|
121
|
+
* and measure timing. Note: query SQL is generated before middleware runs, so
|
|
122
|
+
* modifying params.args in middleware will NOT affect the executed SQL.
|
|
123
|
+
* To intercept queries before SQL generation, use the raw() method instead.
|
|
124
|
+
*
|
|
114
125
|
* @example
|
|
115
126
|
* ```ts
|
|
116
127
|
* // Query timing middleware
|