turbine-orm 0.61.0 → 0.62.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 +65 -21
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +323 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +186 -3
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +125 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +321 -26
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +187 -4
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +124 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/cli/config.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { PASSWORD_QUERY_PARAM_PATTERN } from './ui.js';
|
|
10
11
|
/**
|
|
11
12
|
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
12
13
|
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
@@ -190,9 +191,79 @@ export function resolveSeedFile(config, cwd = process.cwd()) {
|
|
|
190
191
|
// ---------------------------------------------------------------------------
|
|
191
192
|
// Config file template (for `turbine init`)
|
|
192
193
|
// ---------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Does this connection string carry a password?
|
|
196
|
+
*
|
|
197
|
+
* `turbine init --url` used to inline whatever it was given straight into
|
|
198
|
+
* `turbine.config.ts`, a file projects commit, so the documented one-liner
|
|
199
|
+
* (`turbine init --url postgres://user:PASSWORD@host/db`) committed a live
|
|
200
|
+
* database password. Everything that decides between "inline the string" and
|
|
201
|
+
* "read `process.env.DATABASE_URL`" asks this function, so there is exactly one
|
|
202
|
+
* definition of "this value is a secret".
|
|
203
|
+
*
|
|
204
|
+
* Three spellings carry a password, and libpq (and `pg-connection-string`,
|
|
205
|
+
* which is what `pg` actually parses with) accepts all three:
|
|
206
|
+
*
|
|
207
|
+
* 1. URL userinfo: `postgres://user:pass@host/db`.
|
|
208
|
+
* 2. URL query parameter: `postgres://user@host/db?password=pass`, and its
|
|
209
|
+
* siblings such as `?sslpassword=`. This one was missed, on the reasoning
|
|
210
|
+
* that a successful `new URL` ruled out the keyword form below, which is
|
|
211
|
+
* true and irrelevant: the secret was in the query string.
|
|
212
|
+
* 3. libpq keyword form: `host=... password=...` (also `sslpassword=`), which
|
|
213
|
+
* is not a URL at all.
|
|
214
|
+
*
|
|
215
|
+
* The query-parameter test reuses `PASSWORD_QUERY_PARAM_PATTERN`, the very
|
|
216
|
+
* regex `redactUrl` redacts with, so a spelling the terminal output hides can
|
|
217
|
+
* never be a spelling this function calls safe to commit. `pg-connection-string`
|
|
218
|
+
* itself would be the ideal oracle, but it is only a TRANSITIVE dependency (of
|
|
219
|
+
* `pg`), and importing an undeclared package would break on any strict,
|
|
220
|
+
* non-hoisting installer. Detection stays self-contained.
|
|
221
|
+
*
|
|
222
|
+
* @internal exported for tests.
|
|
223
|
+
*/
|
|
224
|
+
export function connectionStringHasPassword(connectionString) {
|
|
225
|
+
const value = connectionString.trim();
|
|
226
|
+
if (!value)
|
|
227
|
+
return false;
|
|
228
|
+
try {
|
|
229
|
+
const parsed = new URL(value);
|
|
230
|
+
// An empty password ("postgres://user@host/db", "?password=") is not a
|
|
231
|
+
// secret. decodeURIComponent is deliberately NOT applied: presence is all
|
|
232
|
+
// we need, and a percent-encoded value is still a non-empty one.
|
|
233
|
+
if (parsed.password !== '')
|
|
234
|
+
return true;
|
|
235
|
+
for (const match of value.matchAll(new RegExp(PASSWORD_QUERY_PARAM_PATTERN, 'gi'))) {
|
|
236
|
+
if (match[2] !== '')
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
// A string libpq parses as a URI is never re-parsed as the keyword form, so
|
|
240
|
+
// the check below genuinely cannot apply here.
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Not a URL: fall through to the libpq keyword form.
|
|
245
|
+
}
|
|
246
|
+
// A keyword whose name ends in `password` (`password`, `sslpassword`) with a
|
|
247
|
+
// non-empty value, at the start of the string or after whitespace.
|
|
248
|
+
return /(^|\s)[^\s=]*password\s*=\s*\S/i.test(value);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The `turbine.config.ts` scaffold.
|
|
252
|
+
*
|
|
253
|
+
* A password-bearing `connectionString` is NEVER inlined: the emitted config
|
|
254
|
+
* reads `process.env.DATABASE_URL` instead, and `turbine init` scaffolds the
|
|
255
|
+
* `.env` that holds the real value. The refusal lives here rather than at the
|
|
256
|
+
* call site so no future caller can reintroduce the leak by passing the raw
|
|
257
|
+
* `--url` through.
|
|
258
|
+
*/
|
|
193
259
|
export function configTemplate(connectionString) {
|
|
194
|
-
const
|
|
195
|
-
|
|
260
|
+
const inlineUrl = connectionString && !connectionStringHasPassword(connectionString) ? connectionString : undefined;
|
|
261
|
+
// Single quotes are the string delimiter in the emitted TS, so a connection
|
|
262
|
+
// string containing one would otherwise produce a config file that does not
|
|
263
|
+
// parse (a password-free URL can still carry a quote in a query parameter).
|
|
264
|
+
const urlLine = inlineUrl
|
|
265
|
+
? ` url: '${inlineUrl.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}',`
|
|
266
|
+
: ` url: process.env.DATABASE_URL,`;
|
|
196
267
|
return `import type { TurbineCliConfig } from 'turbine-orm/cli';
|
|
197
268
|
|
|
198
269
|
/**
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ export interface CliArgs {
|
|
|
67
67
|
yes?: boolean;
|
|
68
68
|
/** `init --skip-schema`: don't scaffold the schema file. */
|
|
69
69
|
skipSchema?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* `init --with-schema`: scaffold the starter schema file even when the
|
|
72
|
+
* database already has tables. The escape hatch for a code-first project
|
|
73
|
+
* bootstrapping against a populated database.
|
|
74
|
+
*/
|
|
75
|
+
withSchema?: boolean;
|
|
70
76
|
/** `init --skip-seed`: don't scaffold the seed file or offer to run it. */
|
|
71
77
|
skipSeed?: boolean;
|
|
72
78
|
/** `init --skip-push`: don't offer to push the schema to the database. */
|
|
@@ -241,7 +247,9 @@ export type InitStepId = 'config' | 'schema' | 'seed-file' | 'push' | 'generate'
|
|
|
241
247
|
/** What the planner decided to do with a step. */
|
|
242
248
|
export type InitStepAction = 'run' | 'prompt' | 'skip';
|
|
243
249
|
/** Why a step was skipped (only set when `action` is `skip`). */
|
|
244
|
-
export type InitStepSkipReason = 'exists' | 'flag' | 'no-url' | 'unreachable' | 'no-seed-file' | 'non-interactive' | 'default-no'
|
|
250
|
+
export type InitStepSkipReason = 'exists' | 'flag' | 'no-url' | 'unreachable' | 'no-seed-file' | 'non-interactive' | 'default-no'
|
|
251
|
+
/** The database already has tables, so an EMPTY code-first schema file is wrong. */
|
|
252
|
+
| 'db-has-tables';
|
|
245
253
|
export interface InitPlanStep {
|
|
246
254
|
id: InitStepId;
|
|
247
255
|
action: InitStepAction;
|
|
@@ -256,6 +264,12 @@ export interface InitPlanState {
|
|
|
256
264
|
seedFileExists: boolean;
|
|
257
265
|
hasUrl: boolean;
|
|
258
266
|
dbReachable: boolean;
|
|
267
|
+
/**
|
|
268
|
+
* The reachable database already contains user tables. Optional: absent means
|
|
269
|
+
* "not probed / unknown", which keeps the pre-0.62 plan for every caller that
|
|
270
|
+
* does not supply it.
|
|
271
|
+
*/
|
|
272
|
+
dbHasTables?: boolean;
|
|
259
273
|
}
|
|
260
274
|
/** Effective flags for the planner. */
|
|
261
275
|
export interface InitPlanFlags {
|
|
@@ -263,6 +277,11 @@ export interface InitPlanFlags {
|
|
|
263
277
|
force: boolean;
|
|
264
278
|
interactive: boolean;
|
|
265
279
|
skipSchema: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* `--with-schema`: scaffold the schema file even against a database that
|
|
282
|
+
* already has tables. Loses to `skipSchema`, which is the explicit "don't".
|
|
283
|
+
*/
|
|
284
|
+
withSchema?: boolean;
|
|
266
285
|
skipSeed: boolean;
|
|
267
286
|
skipPush: boolean;
|
|
268
287
|
skipGenerate: boolean;
|
|
@@ -284,6 +303,63 @@ export interface InitPlanFlags {
|
|
|
284
303
|
* skipped when there is no URL or the database is unreachable.
|
|
285
304
|
*/
|
|
286
305
|
export declare function planInitSteps(state: InitPlanState, flags: InitPlanFlags): InitPlanStep[];
|
|
306
|
+
/** What the secret-handling scaffold decided to do with one file. */
|
|
307
|
+
export type EnvScaffoldAction = 'created' | 'appended' | 'unchanged';
|
|
308
|
+
/** Detected state of the three files the scaffold touches (all IO by the caller). */
|
|
309
|
+
export interface EnvScaffoldState {
|
|
310
|
+
envExists: boolean;
|
|
311
|
+
/** `.env` already assigns DATABASE_URL (their value wins; we never rewrite it). */
|
|
312
|
+
envHasDatabaseUrl: boolean;
|
|
313
|
+
envExampleExists: boolean;
|
|
314
|
+
gitignoreExists: boolean;
|
|
315
|
+
gitignoreIgnoresEnv: boolean;
|
|
316
|
+
}
|
|
317
|
+
export interface EnvScaffoldPlan {
|
|
318
|
+
env: EnvScaffoldAction;
|
|
319
|
+
envExample: EnvScaffoldAction;
|
|
320
|
+
gitignore: EnvScaffoldAction;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Does this `.gitignore` text already ignore `.env`?
|
|
324
|
+
*
|
|
325
|
+
* Line-based rather than a substring search: a `.gitignore` mentioning
|
|
326
|
+
* `.env.example` (a very common line, since the example file is the one you DO
|
|
327
|
+
* commit) contains the text `.env` while ignoring nothing of the kind, and
|
|
328
|
+
* treating that as covered is how the password would stay committable.
|
|
329
|
+
* Negations (`!.env`) are honored as a later line overriding an earlier one,
|
|
330
|
+
* exactly as git resolves them.
|
|
331
|
+
*
|
|
332
|
+
* @internal exported for tests.
|
|
333
|
+
*/
|
|
334
|
+
export declare function gitignoreIgnoresEnv(content: string): boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Decide what the `--url`-carries-a-password scaffold writes. Pure, so the whole
|
|
337
|
+
* matrix (fresh project, existing `.env`, existing `.gitignore`, re-run) is
|
|
338
|
+
* testable without a filesystem.
|
|
339
|
+
*
|
|
340
|
+
* An existing `DATABASE_URL` in `.env` is NEVER rewritten: it is the value the
|
|
341
|
+
* project already runs against, and silently repointing it at the `--url` from
|
|
342
|
+
* one command line is a worse failure than printing a notice.
|
|
343
|
+
*
|
|
344
|
+
* @internal exported for tests.
|
|
345
|
+
*/
|
|
346
|
+
export declare function planEnvScaffold(state: EnvScaffoldState): EnvScaffoldPlan;
|
|
347
|
+
/**
|
|
348
|
+
* Move a password-bearing `--url` out of `turbine.config.ts` and into `.env`.
|
|
349
|
+
*
|
|
350
|
+
* `turbine init --url postgres://user:PASSWORD@host/db` is the documented
|
|
351
|
+
* one-liner, and it used to inline that string verbatim into a file projects
|
|
352
|
+
* commit, with no `.gitignore` written at all. The config template now refuses
|
|
353
|
+
* to inline a secret (see `configTemplate`), so the real value has to land
|
|
354
|
+
* somewhere the config can read it from: this writes `.env`, scaffolds the
|
|
355
|
+
* committable `.env.example` next to it, and makes sure `.gitignore` covers
|
|
356
|
+
* `.env` BEFORE the secret is on disk long enough to be staged.
|
|
357
|
+
*
|
|
358
|
+
* Paths are cwd-relative, exactly like the rest of the init scaffold.
|
|
359
|
+
*
|
|
360
|
+
* @internal exported for tests.
|
|
361
|
+
*/
|
|
362
|
+
export declare function scaffoldEnvForUrl(url: string): EnvScaffoldPlan;
|
|
287
363
|
/**
|
|
288
364
|
* The one-line connection heads-up `turbine init` opens with.
|
|
289
365
|
*
|
|
@@ -366,3 +442,11 @@ export declare function getSeedExecutionPlan(seedFile: string): SeedExecutionPla
|
|
|
366
442
|
* `--allow-remote`. Accepts IPv4, IPv6, and the common bracket form.
|
|
367
443
|
*/
|
|
368
444
|
export declare function isLoopbackHost(host: string): boolean;
|
|
445
|
+
/**
|
|
446
|
+
* Print `<command> --help` for a command that has real help, returning whether
|
|
447
|
+
* one existed. Falling through to the GLOBAL help is the failure mode this map
|
|
448
|
+
* guards against, and it is silent, so the coverage is asserted in tests.
|
|
449
|
+
*
|
|
450
|
+
* @internal exported for tests.
|
|
451
|
+
*/
|
|
452
|
+
export declare function showSubcommandHelp(command: string): boolean;
|